Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
179 changes: 179 additions & 0 deletions rules/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,118 @@ Rules follow Semgrep syntax and concepts:
- External references (OWASP, CWE, upstream rule sources)
- Optional `license` and `provenance`

### Whole-Object Taint: the `$*VAR` Star Operator

A metavariable occurrence in pattern text can be **starred** — `$*VAR` — to mark it as
**whole-object** taint scope: the metavariable's value *and* all of its nested fields, at
any depth (`{ $VAR, $VAR.* }`), instead of just the value itself.

- **The star is a prefix**, bound directly onto the metavariable token right after the `$`:
`$*X` is the star operator. Because the star sits inside the metavar name, there is no
ambiguity with multiplication — both `$X * y` and the adjacent `$X*y` stay ordinary
multiplication (the retired suffix form `$X*` no longer means whole-object taint).
- **Where it's valid**: any metavariable occurrence inside pattern text —
`pattern-sources`, `pattern-sinks`, `pattern-sanitizers`, `pattern-propagators`,
`pattern-not` / `pattern-not-inside`. It's a per-occurrence annotation, not part of the
metavariable's identity: `$X` and `$*X` in the same rule still bind to the same value.
- **Not valid** in the `focus-metavariable` YAML field — that field always stays a plain,
starless name.
- Per operation: a starred **source** taints the value and all its fields; a starred
**sink**/condition matches if the value *or* any of its fields is tainted; a starred
**sanitizer** clears taint on the value and all its fields; a starred **propagator**
copies taint from/to the value and all its fields on the starred side.

Example — a sink that should fire when a *field* of the returned object is tainted, not
just the top-level value:

```yaml
# before: only matches when $X itself carries a taint mark
pattern-sinks:
- patterns:
- pattern: return $X;
```

```yaml
# after: also matches when a nested field of the returned object is tainted
pattern-sinks:
- patterns:
- pattern: return $*X;
```

#### Sinks: the star only takes effect under `focus-metavariable`

For a starred **sink** metavar to actually widen the check, the occurrence must be pinned
with `focus-metavariable`. A bare `pattern` with no focus collapses the sink to a generic
"is *any* argument tainted" position check, which ignores the star entirely — starring the
metavar in that shape is a no-op.

```yaml
# correct: focus-metavariable pins $Y as the sink position, so $*Y is honored
pattern-sinks:
- patterns:
- pattern: Sink($*Y)
- focus-metavariable: $Y
```

This applies to both Java and Go rules.

#### `pattern-not` and the star operator (current limitation)

`pattern-not` is a structural code-shape restriction, not a taint-scope annotation, but its
support for the star operator is currently limited. When a `pattern-not` occurrence shares a
taint metavar with a positive occurrence at the *same position*, the star must match:

- `pattern-not $*X` against a positive `$*X` — supported, excludes the match.
- `pattern-not $X` against a positive plain `$X` — supported (unstarred/unstarred), excludes
the match.
- A positive `$*X` combined with an **unstarred** `pattern-not $X` at the same position is
**not yet supported**. The scoped "keep the field, drop the base" semantics this would
imply isn't implemented; the analyzer emits a non-fatal load-time diagnostic and, for now,
treats the combination as a full (exclude-all) match — the rule still loads.

If your positive occurrence is starred, star the corresponding `pattern-not` occurrence too:

```yaml
# not yet supported: emits a load-time diagnostic, treated as a full exclusion
pattern-sources:
- patterns:
- pattern: |
$METHOD(..., @PathVariable $TYPE $*UNTRUSTED, ...) { ... }
- pattern-not: |
$METHOD(..., @PathVariable $TYPE $UNTRUSTED, ...) { ... }
```

```yaml
# write this instead — star the pattern-not occurrence to match the positive
pattern-sources:
- patterns:
- pattern: |
$METHOD(..., @PathVariable $TYPE $*UNTRUSTED, ...) { ... }
- pattern-not: |
$METHOD(..., @PathVariable $TYPE $*UNTRUSTED, ...) { ... }
```

A scoped exclusion (drop only the field-taint arm while keeping the base-value arm live) is a
possible future refinement — it is not implemented today.

#### Go support

`$*VAR` works in Go rules with the same semantics as Java — `$X` is base-only taint, `$*X` is
base-plus-all-nested-fields — across `pattern-sources`, `pattern-sinks`, and
`pattern-sanitizers`.

**Behavior change for existing Go rules:** plain `$X` sink checks are now strictly
base-only. Previously, a Go sink's `$X` matched coarsely (base value *or* any field/struct/map
taint on it). If a Go rule relies on field-taint matching at a sink, it must now star the
occurrence (`$*X`) to keep matching — see the [Migration Notes](#migration-notes) below.

#### Known limitations

- **The `pattern-not` coincidence diagnostic only fires for method-signature-level
coincidences** (e.g. a `pattern-not` on the same formal-parameter position as the starred
positive, as in the example above) — not for call-argument-shaped coincidences. The latter
still safely resolve to a full exclusion, but without the load-time diagnostic.

---

## Testing and Rule Coverage
Expand Down Expand Up @@ -262,6 +374,73 @@ When introducing or changing rules, follow these guidelines:

---

## Migration Notes

### Spring controller-return sinks: implicit whole-object taint removed

Previously, OpenTaint's Spring integration applied an **implicit** whole-object/any-field
widening to *every* controller-return taint sink, via a hardcoded internal mechanism
(`SpringRuleProvider`) that rewrote any method-exit sink whose position was the return
value into an any-field check — regardless of whether the rule itself asked for it. The
same mechanism implicitly tainted every field of a Spring controller-parameter source, not
just the parameter value.

That hardcoded mechanism has been **removed**. The bundled Spring rules that relied on it
(`spring-response-injection-sink`, `spring-xss-html-response-sink`,
`spring-unvalidated-redirect-sink`, and the Spring untrusted-data/path sources) have been
updated to opt in explicitly with the `$*VAR` star operator described above, so their
behavior is unchanged.

**If you maintain custom rules**, this is a behavior change to be aware of: a custom rule
with a return-value sink inside a Spring controller —

```yaml
pattern-sinks:
- patterns:
- pattern: return $X;
```

— **no longer implicitly matches** when only a field of the returned object is tainted
(rather than `$X` itself). To restore that behavior, star the occurrence:

```yaml
pattern-sinks:
- patterns:
- pattern: return $*X;
```

Likewise, a custom source rule matching a Spring controller parameter now taints only the
parameter value unless you star the occurrence (`$*VAR`) to also taint its fields.

### Go: sink `$X` is now strictly base-only

Go's `$*VAR` star operator support (see above) came with a related default-semantics fix:
previously, a Go sink pattern's plain `$X` matched coarsely — it fired on taint anywhere on
the value, including its fields, structs, and maps. That coarse default has been corrected:
a plain `$X` sink now checks the base value only, matching Java's semantics.

**If you maintain custom Go rules**, this is a behavior change to be aware of: a sink rule
that used to rely on `$X` catching field/struct/map taint —

```yaml
pattern-sinks:
- patterns:
- pattern: Sink($X)
- focus-metavariable: $X
```

— no longer matches when only a field of `$X` is tainted. Star the occurrence to restore
that behavior:

```yaml
pattern-sinks:
- patterns:
- pattern: Sink($*X)
- focus-metavariable: $X
```

---

## License

This project is released under the [MIT License](LICENSE).
Expand Down
4 changes: 2 additions & 2 deletions rules/ruleset/go/lib/cmdi-sinks.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,8 @@ rules:
import "os/exec"
...
- pattern-either:
- pattern: "exec.Command(\"$NAME\", ..., $UNTRUSTED, ...)"
- pattern: "exec.CommandContext($CTX, \"$NAME\", ..., $UNTRUSTED, ...)"
- pattern: "exec.Command(\"$NAME\", ..., $*UNTRUSTED, ...)"
- pattern: "exec.CommandContext($CTX, \"$NAME\", ..., $*UNTRUSTED, ...)"
- metavariable-regex:
metavariable: $NAME
regex: sh
Expand Down
4 changes: 2 additions & 2 deletions rules/ruleset/go/lib/ssti-sinks.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ rules:
- pattern: "template.New($N).Parse($UNTRUSTED)"
- pattern: "template.Must(template.New($N).Parse($UNTRUSTED))"
- pattern: "($T : *template.Template).Parse($UNTRUSTED)"
- pattern: "template.New($N).ParseFiles($UNTRUSTED, ...)"
- pattern: "template.New($N).ParseFiles($*UNTRUSTED, ...)"
- pattern: "template.New($N).ParseGlob($UNTRUSTED)"
- focus-metavariable: $UNTRUSTED
- patterns:
Expand All @@ -32,6 +32,6 @@ rules:
- pattern: "template.New($N).Parse($UNTRUSTED)"
- pattern: "template.Must(template.New($N).Parse($UNTRUSTED))"
- pattern: "($T : *template.Template).Parse($UNTRUSTED)"
- pattern: "template.New($N).ParseFiles($UNTRUSTED, ...)"
- pattern: "template.New($N).ParseFiles($*UNTRUSTED, ...)"
- pattern: "template.New($N).ParseGlob($UNTRUSTED)"
- focus-metavariable: $UNTRUSTED
6 changes: 3 additions & 3 deletions rules/ruleset/go/lib/xss-sinks.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -29,10 +29,10 @@ rules:
- pattern: "($O : *context.BeegoOutput).Body($UNTRUSTED)"
- pattern: "($O : *context.BeegoOutput).JSON($UNTRUSTED, ...)"
- pattern: "($UNTRUSTED : *web.Controller).ServeJSON()"
- pattern: "fmt.Fprint($W, $UNTRUSTED)"
- pattern: "fmt.Fprint($W, $*UNTRUSTED)"
- pattern: "fmt.Fprintf($W, $UNTRUSTED, ...)"
- pattern: "fmt.Fprintf($W, $FORMAT, ..., $UNTRUSTED, ...)"
- pattern: "fmt.Fprintln($W, $UNTRUSTED)"
- pattern: "fmt.Fprintf($W, $FORMAT, ..., $*UNTRUSTED, ...)"
- pattern: "fmt.Fprintln($W, $*UNTRUSTED)"
- pattern: "io.WriteString($W, $UNTRUSTED)"
- pattern: "json.NewEncoder($W).Encode($UNTRUSTED)"
- focus-metavariable: $UNTRUSTED
Expand Down
14 changes: 11 additions & 3 deletions rules/ruleset/go/security/trust-boundary.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -129,11 +129,19 @@ rules:
- pattern: "($X : *context.BeegoInput).RequestBody"
- pattern: "($X : *context.BeegoInput).Bind($PTR)"
pattern-sinks:
# Star $C so a tainted cookie FIELD (Value/Path set from user input) is observed on the
# whole *http.Cookie via the any-field check.
- patterns:
- pattern-inside: |
import "net/http"
import web "github.com/beego/beego/v2/server/web"
...
- pattern-either:
- pattern: "http.SetCookie($W, $C)"
- pattern: "($C : *web.Controller).SetSession($K, $V)"
- pattern: "http.SetCookie($W, $*C)"
- focus-metavariable: $C
- patterns:
- pattern-inside: |
import "net/http"
import web "github.com/beego/beego/v2/server/web"
...
- pattern: "($C : *web.Controller).SetSession($K, $*V)"
- focus-metavariable: $V
91 changes: 48 additions & 43 deletions rules/ruleset/java/lib/generic/code-injection-sinks.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -12,46 +12,47 @@ rules:
- java
patterns:
- pattern-either:
- pattern: ognl.Ognl.getValue($INPUT,...);
- pattern: (com.opensymphony.xwork2.ognl.OgnlReflectionProvider $P).getGetMethod($T, $INPUT,...);
- pattern: (com.opensymphony.xwork2.ognl.OgnlReflectionProvider $P).getSetMethod($T, $INPUT,...);
- pattern: (com.opensymphony.xwork2.ognl.OgnlReflectionProvider $P).getField($T, $INPUT,...);
- pattern: (com.opensymphony.xwork2.ognl.OgnlReflectionProvider $P).setProperties($INPUT,...);
- pattern: (com.opensymphony.xwork2.ognl.OgnlReflectionProvider $P).setProperty($INPUT,...);
- pattern: (com.opensymphony.xwork2.ognl.OgnlReflectionProvider $P).getValue($INPUT,...);
- pattern: (com.opensymphony.xwork2.ognl.OgnlReflectionProvider $P).setValue($INPUT,...);
- pattern: (com.opensymphony.xwork2.util.reflection.ReflectionProvider $P).getGetMethod($T, $INPUT,...);
- pattern: (com.opensymphony.xwork2.util.reflection.ReflectionProvider $P).getSetMethod($T, $INPUT,...);
- pattern: (com.opensymphony.xwork2.util.reflection.ReflectionProvider $P).getField($T, $INPUT,...);
- pattern: (com.opensymphony.xwork2.util.reflection.ReflectionProvider $P).setProperties($INPUT,...);
- pattern: (com.opensymphony.xwork2.util.reflection.ReflectionProvider $P).setProperty($INPUT,...);
- pattern: (com.opensymphony.xwork2.util.reflection.ReflectionProvider $P).getValue($INPUT,...);
- pattern: (com.opensymphony.xwork2.util.reflection.ReflectionProvider $P).setValue($INPUT,...);
- pattern: (com.opensymphony.xwork2.util.reflection.ReflectionProvider $P).translateVariables($INPUT,...);
- pattern: com.opensymphony.xwork2.util.TextParseUtil.translateVariables($INPUT, ...);
- pattern: com.opensymphony.xwork2.util.TextParseUtil.translateVariablesCollection($INPUT,...);
- pattern: com.opensymphony.xwork2.util.TextParseUtil.shallBeIncluded($INPUT,...);
- pattern: ognl.Ognl.getValue($*INPUT,...);
- pattern: (com.opensymphony.xwork2.ognl.OgnlReflectionProvider $P).getGetMethod($T, $*INPUT,...);
- pattern: (com.opensymphony.xwork2.ognl.OgnlReflectionProvider $P).getSetMethod($T, $*INPUT,...);
- pattern: (com.opensymphony.xwork2.ognl.OgnlReflectionProvider $P).getField($T, $*INPUT,...);
- pattern: (com.opensymphony.xwork2.ognl.OgnlReflectionProvider $P).setProperties($*INPUT,...);
- pattern: (com.opensymphony.xwork2.ognl.OgnlReflectionProvider $P).setProperty($*INPUT,...);
- pattern: (com.opensymphony.xwork2.ognl.OgnlReflectionProvider $P).getValue($*INPUT,...);
- pattern: (com.opensymphony.xwork2.ognl.OgnlReflectionProvider $P).setValue($*INPUT,...);
- pattern: (com.opensymphony.xwork2.util.reflection.ReflectionProvider $P).getGetMethod($T, $*INPUT,...);
- pattern: (com.opensymphony.xwork2.util.reflection.ReflectionProvider $P).getSetMethod($T, $*INPUT,...);
- pattern: (com.opensymphony.xwork2.util.reflection.ReflectionProvider $P).getField($T, $*INPUT,...);
- pattern: (com.opensymphony.xwork2.util.reflection.ReflectionProvider $P).setProperties($*INPUT,...);
- pattern: (com.opensymphony.xwork2.util.reflection.ReflectionProvider $P).setProperty($*INPUT,...);
- pattern: (com.opensymphony.xwork2.util.reflection.ReflectionProvider $P).getValue($*INPUT,...);
- pattern: (com.opensymphony.xwork2.util.reflection.ReflectionProvider $P).setValue($*INPUT,...);
- pattern: (com.opensymphony.xwork2.util.reflection.ReflectionProvider $P).translateVariables($*INPUT,...);
- pattern: com.opensymphony.xwork2.util.TextParseUtil.translateVariables($*INPUT, ...);
- pattern: com.opensymphony.xwork2.util.TextParseUtil.translateVariablesCollection($*INPUT,...);
- pattern: com.opensymphony.xwork2.util.TextParseUtil.shallBeIncluded($*INPUT,...);
# TODO: commaDelimitedStringToSet is propagator!
- pattern: com.opensymphony.xwork2.util.TextParseUtil.commaDelimitedStringToSet($INPUT,...);
- pattern: (com.opensymphony.xwork2.util.OgnlTextParser $P).evaluate($INPUT,...);
- pattern: (com.opensymphony.xwork2.util.OgnlTextParser $P).setProperties($INPUT,...);
- pattern: (com.opensymphony.xwork2.ognl.OgnlUtil $P).setProperty($INPUT,...);
- pattern: (com.opensymphony.xwork2.ognl.OgnlUtil $P).getValue($INPUT,...);
- pattern: (com.opensymphony.xwork2.ognl.OgnlUtil $P).setValue($INPUT,...);
- pattern: (com.opensymphony.xwork2.ognl.OgnlUtil $P).callMethod($INPUT,...);
- pattern: (com.opensymphony.xwork2.ognl.OgnlUtil $P).compile($INPUT,...);
- pattern: (org.apache.struts2.util.VelocityStrutsUtil $P).evaluate($INPUT,...);
- pattern: (org.apache.struts2.util.StrutsUtil $P).isTrue($INPUT,...);
- pattern: (org.apache.struts2.util.StrutsUtil $P).findString($INPUT,...);
- pattern: (org.apache.struts2.util.StrutsUtil $P).findValue($INPUT,...);
- pattern: (org.apache.struts2.util.StrutsUtil $P).getText($INPUT,...);
- pattern: (org.apache.struts2.util.StrutsUtil $P).translateVariables($INPUT,...);
- pattern: (org.apache.struts2.util.StrutsUtil $P).makeSelectList($INPUT,...);
- pattern: (org.apache.struts2.views.jsp.ui.OgnlTool $P).findValue($INPUT,...);
- pattern: (com.opensymphony.xwork2.util.ValueStack $P).findString($INPUT,...);
- pattern: (com.opensymphony.xwork2.util.ValueStack $P).findValue($INPUT,...);
- pattern: (com.opensymphony.xwork2.util.ValueStack $P).setValue($INPUT,...);
- pattern: (com.opensymphony.xwork2.util.ValueStack $P).setParameter($INPUT,...);
- pattern: com.opensymphony.xwork2.util.TextParseUtil.commaDelimitedStringToSet($*INPUT,...);
- pattern: (com.opensymphony.xwork2.util.OgnlTextParser $P).evaluate($*INPUT,...);
- pattern: (com.opensymphony.xwork2.util.OgnlTextParser $P).setProperties($*INPUT,...);
- pattern: (com.opensymphony.xwork2.ognl.OgnlUtil $P).setProperty($*INPUT,...);
- pattern: (com.opensymphony.xwork2.ognl.OgnlUtil $P).getValue($*INPUT,...);
- pattern: (com.opensymphony.xwork2.ognl.OgnlUtil $P).setValue($*INPUT,...);
- pattern: (com.opensymphony.xwork2.ognl.OgnlUtil $P).callMethod($*INPUT,...);
- pattern: (com.opensymphony.xwork2.ognl.OgnlUtil $P).compile($*INPUT,...);
- pattern: (org.apache.struts2.util.VelocityStrutsUtil $P).evaluate($*INPUT,...);
- pattern: (org.apache.struts2.util.StrutsUtil $P).isTrue($*INPUT,...);
- pattern: (org.apache.struts2.util.StrutsUtil $P).findString($*INPUT,...);
- pattern: (org.apache.struts2.util.StrutsUtil $P).findValue($*INPUT,...);
- pattern: (org.apache.struts2.util.StrutsUtil $P).getText($*INPUT,...);
- pattern: (org.apache.struts2.util.StrutsUtil $P).translateVariables($*INPUT,...);
- pattern: (org.apache.struts2.util.StrutsUtil $P).makeSelectList($*INPUT,...);
- pattern: (org.apache.struts2.views.jsp.ui.OgnlTool $P).findValue($*INPUT,...);
- pattern: (com.opensymphony.xwork2.util.ValueStack $P).findString($*INPUT,...);
- pattern: (com.opensymphony.xwork2.util.ValueStack $P).findValue($*INPUT,...);
- pattern: (com.opensymphony.xwork2.util.ValueStack $P).setValue($*INPUT,...);
- pattern: (com.opensymphony.xwork2.util.ValueStack $P).setParameter($*INPUT,...);
- focus-metavariable: $INPUT

- id: dangerous-groovy-shell
options:
Expand Down Expand Up @@ -96,7 +97,11 @@ rules:
provenance: https://find-sec-bugs.github.io/bugs.htm#SCRIPT_ENGINE_INJECTION
languages:
- java
pattern-either:
- pattern: (javax.script.ScriptEngine $SE).eval($UNTRUSTED)
- pattern: (javax.script.Invocable $INVC).invokeFunction(..., $UNTRUSTED)
- pattern: (javax.script.Invocable $INVC).invokeMethod(..., $UNTRUSTED)
patterns:
- pattern-either:
- pattern: (javax.script.ScriptEngine $SE).eval($UNTRUSTED)
# invokeFunction/invokeMethod pass args via an Object... vararg, so a tainted arg lands
# as a slice element — star to observe it via any-field.
- pattern: (javax.script.Invocable $INVC).invokeFunction(..., $*UNTRUSTED)
- pattern: (javax.script.Invocable $INVC).invokeMethod(..., $*UNTRUSTED)
- focus-metavariable: $UNTRUSTED
Loading
Loading