diff --git a/docs/dynamic_rules.md b/docs/dynamic_rules.md index f967c40c..8b90068a 100644 --- a/docs/dynamic_rules.md +++ b/docs/dynamic_rules.md @@ -492,7 +492,7 @@ eval(${"var"}); ##### `@filter` -The `@filter` restriction allows you to restrict the rule by name of matched variable. +The `@filter` restriction allows you to restrict the rule by name of matched variable or literal. Thus, the rule will be applied only if there is a matched variable that matches passed regexp. @@ -510,8 +510,6 @@ function forbiddenIdUsage() { This rule will match usage if `$id` variable. -Also, originally designed to apply to variables by matching their names, the functionality has been extended so that if a string literal is captured under the given name, the regular expression is applied to its literal value. - When a string literal is captured, the regular expression is applied to the literal’s value. For example: @@ -528,6 +526,29 @@ function insecureUrl() { In this case, the rule will match because the captured string literal "http://example.com" matches the regular expression ^http://. +Let see more complex example: + +```php +function legacyLibsUsage() { + /** + * @warning Don't use legacy libs + * @filter $file (legacy\.lib) + */ + any_legacy_libs_usage: { + require ${'file:str'}; + require_once ${'file:str'}; + include ${'file:str'}; + include_once ${'file:str'}; + + require __DIR__ . ${'file:str'}; + require_once __DIR__ . ${'file:str'}; + include __DIR__ . ${'file:str'}; + include_once __DIR__ . ${'file:str'}; + } +} +``` + +This regular expression (legacy\.lib) will match any line that contains the substring legacy.lib in the substituted expression of the $file #### Underline location (`@location`) diff --git a/src/linter/root.go b/src/linter/root.go index 03cdf2a7..044cdccf 100644 --- a/src/linter/root.go +++ b/src/linter/root.go @@ -1893,7 +1893,14 @@ func (d *rootWalker) runRule(n ir.Node, sc *meta.Scope, rule *rules.Rule) bool { matched = true } else { for _, filterSet := range rule.Filters { - if d.checkFilterSet(&m, sc, filterSet) { + + filterMatched, errorFilterSet := d.checkFilterSet(&m, sc, filterSet) + if errorFilterSet != nil { + d.Report(n, rule.Level, rule.Name, "%s", errorFilterSet) + return true + } + + if filterMatched { matched = true break } @@ -1950,7 +1957,7 @@ func (d *rootWalker) checkTypeFilter(wantType *phpdoc.Type, sc *meta.Scope, nn i return rules.TypeIsCompatible(wantType.Expr, haveType.Expr) } -func (d *rootWalker) checkFilterSet(m *phpgrep.MatchData, sc *meta.Scope, filterSet map[string]rules.Filter) bool { +func (d *rootWalker) checkFilterSet(m *phpgrep.MatchData, sc *meta.Scope, filterSet map[string]rules.Filter) (bool, error) { // TODO: pass custom types here, so both @type and @pure predicates can use it. for name, filter := range filterSet { @@ -1960,24 +1967,27 @@ func (d *rootWalker) checkFilterSet(m *phpgrep.MatchData, sc *meta.Scope, filter } if !d.checkTypeFilter(filter.Type, sc, nn) { - return false + return false, nil } if filter.Pure && !solver.SideEffectFree(d.scope(), d.ctx.st, nil, nn) { - return false + return false, nil } if filter.Regexp != nil { switch v := nn.(type) { case *ir.SimpleVar: if !filter.Regexp.MatchString(v.Name) { - return false + return false, nil } case *ir.String: if !filter.Regexp.MatchString(v.Value) { - return false + return false, nil } + default: + // logical paradox: we handled it, but does not support that's why here false + return false, fmt.Errorf("applying @filter for construction '%s' does not support. Current supported capturing types are str and var", d.nodeText(nn)) } } } - return true + return true, nil } diff --git a/src/rules/parser.go b/src/rules/parser.go index a713cffe..120ffe00 100644 --- a/src/rules/parser.go +++ b/src/rules/parser.go @@ -363,7 +363,7 @@ func (p *parser) parseRuleInfo(st ir.Node, labelStmt ir.Node, proto *Rule) (Rule return rule, p.errorf(st, "@filter param must be a phpgrep variable") } name = strings.TrimPrefix(name, "$") - found := p.checkForVariableInPattern(name, patternStmt, verifiedVars) + found := p.filterByPattern(name, patternStmt, verifiedVars) if !found { return rule, p.errorf(st, "@filter contains a reference to a variable %s that is not present in the pattern", name) } @@ -558,3 +558,35 @@ func (p *parser) checkForVariableInPattern(name string, pattern ir.Node, verifie return found } + +func (p *parser) filterByPattern(name string, pattern ir.Node, verifiedVars map[string]struct{}) bool { + if _, ok := verifiedVars[name]; ok { + return true + } + + found := irutil.FindWithPredicate(&ir.SimpleVar{Name: name}, pattern, func(what ir.Node, cur ir.Node) bool { + // we can capture anything: vars, const, int and etc: see more in phpgrep doc. Example: + /* + @filter $file ^var + ^^^^^^ <- captured + callApi(${'file:str'}); + ^^^^^^^^ <- patternForFound + */ + if s, ok := cur.(*ir.Var); ok { + if s, ok := s.Expr.(*ir.String); ok { + captured := what.(*ir.SimpleVar).Name + patternForFound := s.Value + + return strings.HasPrefix(patternForFound, captured) + } + } + + return false + }) + + if found { + verifiedVars[name] = struct{}{} + } + + return found +} diff --git a/src/tests/rules/rules_test.go b/src/tests/rules/rules_test.go index a8c919db..5c0c0654 100644 --- a/src/tests/rules/rules_test.go +++ b/src/tests/rules/rules_test.go @@ -780,6 +780,214 @@ function testMultipleEndpoints() { test.RunRulesTest() } +func TestFilterLegacyLibsUsageMatches(t *testing.T) { + rfile := `