@@ -2,11 +2,13 @@ package check
22
33import (
44 "context"
5+ "errors"
56 "fmt"
67 "regexp"
78 "strings"
89
910 "github.com/d5/tengo/v2"
11+ "github.com/d5/tengo/v2/parser"
1012 "github.com/d5/tengo/v2/stdlib"
1113
1214 "github.com/errata-ai/vale/v3/internal/core"
@@ -50,7 +52,12 @@ func NewMetric(_ *core.Config, generic baseCheck, path string) (Metric, error) {
5052// Run calculates the readability level of the given text.
5153func (o Metric ) Run (_ nlp.Block , f * core.File , _ * core.Config ) ([]core.Alert , error ) {
5254 alerts := []core.Alert {}
53- ctx := context .Background ()
55+
56+ // A formula is compiled and run as a Tengo program, so it needs the same
57+ // deadline a script rule gets; see tengoTimeout. Both evalMath calls share
58+ // it, which bounds the rule as a whole rather than each half of it.
59+ ctx , cancel := context .WithTimeout (context .Background (), tengoTimeout )
60+ defer cancel ()
5461
5562 parameters , err := f .ComputeMetrics ()
5663 if err != nil {
@@ -73,15 +80,17 @@ func (o Metric) Run(_ nlp.Block, f *core.File, _ *core.Config) ([]core.Alert, er
7380 // We need this to allow showing the result in a rule's message.
7481 res , err := evalMath (ctx , o .Formula , parameters )
7582 if err != nil {
76- return alerts , core .NewE201FromTarget (err .Error (), "formula" , o .path )
83+ return alerts , ruleError (
84+ o .Name , "formula" , o .path , err , errors .Is (ctx .Err (), context .DeadlineExceeded ))
7785 }
7886
7987 // The binary result of our formula:
8088 eqb := fmt .Sprintf ("%f %s" , res , o .Condition )
8189
8290 match , err := evalMath (ctx , eqb , parameters )
8391 if err != nil {
84- return alerts , core .NewE201FromTarget (err .Error (), "condition" , o .path )
92+ return alerts , ruleError (
93+ o .Name , "condition" , o .path , err , errors .Is (ctx .Err (), context .DeadlineExceeded ))
8594 }
8695
8796 if match .(bool ) {
@@ -105,6 +114,40 @@ func (o Metric) Pattern() string {
105114 return o .Formula
106115}
107116
117+ // checkExpression rejects anything that is not a single expression.
118+ //
119+ // A rule's formula is pasted into a Tengo program by boilerplate above, and
120+ // `%s` escapes nothing. A formula that closes the parenthesis it was handed can
121+ // therefore append statements of its own, which turns a `metric` rule -- meant
122+ // to be arithmetic over a document's counts -- into arbitrary code running in
123+ // the same VM a `script` rule gets. `0); for { } ; x := (0` is the whole exploit,
124+ // and the same applies to `condition`, which is spliced after a number.
125+ //
126+ // Parsing the formula on its own settles it. An injection cannot survive the
127+ // trip: the `)` it depends on has no opener until the boilerplate supplies one,
128+ // so it fails to parse here, where it is still just a string.
129+ func checkExpression (expr string ) error {
130+ fileSet := parser .NewFileSet ()
131+ srcFile := fileSet .AddFile ("expression" , - 1 , len (expr ))
132+
133+ parsed , err := parser .NewParser (srcFile , []byte (expr ), nil ).ParseFile ()
134+ if err != nil {
135+ return fmt .Errorf ("invalid expression %q: %w" , expr , err )
136+ }
137+
138+ if len (parsed .Stmts ) != 1 {
139+ return fmt .Errorf (
140+ "expected a single expression, found %d statements in %q" ,
141+ len (parsed .Stmts ), expr )
142+ }
143+ if _ , ok := parsed .Stmts [0 ].(* parser.ExprStmt ); ! ok {
144+ return fmt .Errorf (
145+ "expected an expression, found %T in %q" , parsed .Stmts [0 ], expr )
146+ }
147+
148+ return nil
149+ }
150+
108151func evalMath (
109152 ctx context.Context ,
110153 expr string ,
@@ -115,6 +158,10 @@ func evalMath(
115158 return nil , fmt .Errorf ("empty expression" )
116159 }
117160
161+ if err := checkExpression (expr ); err != nil {
162+ return nil , err
163+ }
164+
118165 script := tengo .NewScript ([]byte (fmt .Sprintf (boilerplate , expr )))
119166 script .SetImports (stdlib .GetModuleMap ("math" ))
120167
0 commit comments