You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@groovy.apache.org by pa...@apache.org on 2026/06/17 02:47:13 UTC

(groovy-website) branch asf-site updated: draft type checker for tainted data processing.

This is an automated email from the ASF dual-hosted git repository.

paulk-asert pushed a commit to branch asf-site
in repository https://gitbox.apache.org/repos/asf/groovy-website.git


The following commit(s) were added to refs/heads/asf-site by this push:
     new 076424a  draft type checker for tainted data processing.
076424a is described below

commit 076424a8d8501157ee026e518cc385ffd78e3dc9
Author: Paul King <pa...@asert.com.au>
AuthorDate: Wed Jun 17 12:47:04 2026 +1000

    draft type checker for tainted data processing.
---
 site/src/site/wiki/GEP-25.adoc | 474 +++++++++++++++++++++++++++++++++++++++++
 1 file changed, 474 insertions(+)

diff --git a/site/src/site/wiki/GEP-25.adoc b/site/src/site/wiki/GEP-25.adoc
new file mode 100644
index 0000000..e0ea962
--- /dev/null
+++ b/site/src/site/wiki/GEP-25.adoc
@@ -0,0 +1,474 @@
+= GEP-25: Compile-time Taint Tracking (`@Tainted` / `@Untainted`)
+
+:icons: font
+
+.Metadata
+****
+[horizontal,options="compact"]
+*Number*:: GEP-25
+*Title*:: Compile-time Taint Tracking (`@Tainted` / `@Untainted`)
+*Version*:: 1
+*Type*:: Feature
+*Status*:: Draft (placeholder)
+*Leader*:: Paul King
+*Created*:: 2026-06-17
+*Last modification*:: 2026-06-17
+****
+
+[width="80%",align="center"]
+|===
+a| NOTE: _WARNING:_
+This proposal is an early *placeholder*. It stakes out a design and, more importantly, an
+annotation vocabulary, so that the syntax can be aligned in Groovy 6.x ahead of any implementation
+and so the community can gauge appetite. We are currently working on Groovy 6.0 and this proposal
+targets Groovy 7.0. A taint checker is only worth shipping if the project is comfortable with the
+ongoing maintenance it implies (see _Risks, maintenance, and status_); this draft is deliberately
+honest about that. We welcome feedback and discussion, but the details are not finalized and the
+feature may not proceed at all without committed maintainers.
+|===
+
+== Abstract
+
+This GEP proposes an optional, opt-in `@TypeChecked` extension — `TaintChecker` — that performs
+*compile-time taint tracking* over Groovy code, together with two type-qualifier annotations,
+`@Tainted` and `@Untainted`. It joins the existing family of pluggable checkers in the
+`groovy-typecheckers` module (`NullChecker`, `RegexChecker`, `FormatStringChecker`, `PurityChecker`,
+and the shape checkers `CombinerChecker` / `MonadicChecker`).
+
+Taint tracking is the classic security dataflow analysis: data originating from an *untrusted source*
+(request parameters, environment, files, deserialized input) is flagged as `@Tainted`, and the checker
+reports a compile error when tainted data reaches a *sink* that requires `@Untainted` data (a SQL
+query, a shell command, an HTML response, a file path) without first passing through a *sanitizer*.
+The whole analysis is a two-point lattice — `@Untainted <: @Tainted` — which keeps it a fast, sound,
+solver-free dataflow pass rather than a heavyweight verification engine.
+
+Two design choices distinguish this from a straight port of existing Java taint checkers. First, the
+analysis is *aware of Groovy's own structural mechanisms* — most notably `GString`, which already
+gives `groovy.sql.Sql` safe-by-construction, parameterized queries; the checker can recognize and
+reward that existing idiom rather than ignore it. Second, the `@Tainted` / `@Untainted` vocabulary is
+deliberately the *two extremes of a security lattice*, so it remains forward-compatible with richer,
+external information-flow tools without those tools needing their own annotations.
+
+== Motivation
+
+Injection flaws (CWE-89 SQL injection, CWE-79 cross-site scripting, CWE-78 OS command injection,
+CWE-22 path traversal) remain perennially at the top of the OWASP Top Ten. They share one structure:
+untrusted data flows into a trusting operation without sanitization. That structure is exactly what
+taint analysis catches, and catching it *at compile time* — in the same pass as type checking, with
+the developer's IDE underlining the offending flow — is materially cheaper than catching it in a
+separate SAST tool, a code review, or production.
+
+Groovy is a frequent host for precisely the code where this matters: build scripts, server-side web
+handlers (Grails, Micronaut, Ratpack, raw servlets), SQL via `groovy.sql.Sql`, markup via
+`MarkupBuilder` and template engines, and shell-outs via `"cmd".execute()`. A Groovy-native taint
+checker meets that code where it lives.
+
+The broader trajectory matters too. As AI-assisted security tooling — vulnerability detection,
+suggested fixes, automated pull requests — continues to gain momentum, a language that exposes a
+*typed, machine-checkable* taint substrate is far better positioned than one that offers only freeform
+source to reason about. An AI can propose `@Tainted` / `@Untainted` / sanitizer annotations; the
+compiler then *verifies the resulting flow soundly*. The model proposes, the checker disposes. That
+turns a probabilistic suggestion into a checked guarantee, and it gives AI-generated Groovy code a
+compile-time backstop against the injection bugs such code is prone to. Putting the substrate in place
+now is what lets Groovy ride those advances rather than retrofit them later.
+
+=== Design principles
+
+* *Opt-in and composable* — like every other checker in the family, `TaintChecker` is enabled per
+  class or per method via
+  `@TypeChecked(extensions = ['groovy.typecheckers.TaintChecker'])`, and composes with the other
+  extensions on the same compile. Code that does not opt in is completely unaffected.
+
+* *Low-noise by default* — a taint checker that cries wolf is uninstalled. The default qualifier is
+  `@Untainted`; only *known* sources are `@Tainted` (via the shipped model library or user
+  annotation), so a diagnostic fires only when data the checker *knows* is tainted reaches a sink.
+  This trades a measure of soundness (an un-modeled source is missed) for trust, with a stricter,
+  sound-by-default mode available behind a flag.
+
+* *Two annotations, role by position* — `@Tainted` and `@Untainted` are the entire surface. A source
+  is a `@Tainted` return; a sink is an `@Untainted` parameter; a sanitizer is an `@Untainted` return
+  that accepts `@Tainted` input. There is no separate `@Source` / `@Sink` / `@Sanitizer` vocabulary
+  and no lattice-configuration machinery — a two-point order needs none.
+
+* *Reward existing Groovy safety* — Groovy already has safe-by-construction idioms (GString-based
+  parameterized SQL, escaping builders). The checker recognizes these as built-in sanitizing sinks
+  rather than forcing developers to annotate around them.
+
+* *Honest scope, loud where it must skip* — Groovy's dynamic dispatch cannot always be tracked
+  statically. Where the checker cannot follow a flow it says so (a "flow not tracked" note) rather
+  than silently passing or silently failing, mirroring the honesty of the sibling checkers.
+
+* *Forward-compatible vocabulary* — `@Tainted` / `@Untainted` are the top and bottom of a security
+  lattice. Richer external analyses can consume the same annotations as the extremes of a finer
+  ordering, so annotating for `TaintChecker` is never wasted effort.
+
+== The annotations
+
+Two annotations, usable in `TYPE_USE` position (and on methods, parameters, and fields):
+
+[source,groovy]
+----
+package groovy.transform        // package tentative
+
+@Documented @Retention(RUNTIME) @Target([TYPE_USE, METHOD, PARAMETER, FIELD, LOCAL_VARIABLE])
+@interface Tainted {}
+
+@Documented @Retention(RUNTIME) @Target([TYPE_USE, METHOD, PARAMETER, FIELD, LOCAL_VARIABLE])
+@interface Untainted {}
+----
+
+The qualifier lattice is two points with `@Untainted <: @Tainted`: an untainted value may be used
+wherever a tainted value is accepted, but not the reverse. The role an annotation plays is determined
+entirely by where it appears:
+
+[cols="2,3"]
+|===
+|Position |Role
+
+|`@Tainted` return type
+|*Source* — the method yields untrusted data (`request.getParameter(...)`)
+
+|`@Untainted` parameter
+|*Sink* — the method requires trusted data (`Statement.executeQuery(String)`)
+
+|`@Untainted` return, `@Tainted` (or any) input
+|*Sanitizer* — the method launders taint (`escapeHtml(String)`)
+
+|`@Tainted` / `@Untainted` on a field, local, or value
+|A tracked value of that qualifier
+|===
+
+This is the whole model. There is no separate declassifier annotation: an `@Untainted` return *is* the
+declassification, because in a two-point world there is only one direction worth naming
+(tainted → untainted).
+
+== Features
+
+=== Sources, sinks, and the basic flow
+
+[source,groovy]
+----
+@TypeChecked(extensions = ['groovy.typecheckers.TaintChecker'])
+class Handler {
+    @Tainted String userName(HttpServletRequest req) {
+        req.getParameter('name')              // request params are modeled @Tainted
+    }
+
+    void run(Sql sql, HttpServletRequest req) {
+        @Tainted String name = userName(req)
+
+        // ERROR: a @Tainted String reaches a query sink built by concatenation
+        sql.execute('SELECT * FROM users WHERE name = \'' + name + '\'')
+    }
+}
+----
+
+The diagnostic names the flow, in the style of the other checkers:
+
+----
+[Static type checking] - Tainted value 'name' reaches the untainted parameter of Sql.execute;
+data from Handler.userName (a @Tainted source) flows here without sanitization
+----
+
+=== Sanitizers (the only declassification needed)
+
+A method whose return is `@Untainted` launders taint — the checker trusts the annotation and treats
+the result as untainted regardless of input:
+
+[source,groovy]
+----
+@Untainted String escapeHtml(@Tainted String s) { /* ... escaping ... */ }
+
+void render(Writer out, @Tainted String comment) {
+    out.write(escapeHtml(comment))            // OK: the sink receives untainted data
+}
+----
+
+For the rare audited-inline case — "I have validated this value by other means" — an explicit,
+greppable suppression is provided rather than a silent cast:
+
+[source,groovy]
+----
+@SuppressWarnings('groovy.taint')
+@Untainted String trusted = afterMyOwnValidation(raw)
+----
+
+Suppressions are the audit trail: a security reviewer greps `groovy.taint` to find every point where
+a human overrode the analysis, exactly as `Declassify`-style markers serve in richer tools.
+
+=== Propagation and flow sensitivity
+
+Taint propagates through the expression grammar: assignment, string concatenation and `GString`
+interpolation (see below), arithmetic, and method calls (a value derived from tainted arguments is
+tainted unless the callee declares an `@Untainted` return). The analysis is *flow-sensitive*: a
+variable may be tainted at one program point and untainted at a later one after sanitization —
+
+[source,groovy]
+----
+@Tainted String x = source()
+sink(x)                 // ERROR
+x = escapeHtml(x)       // x is @Untainted from here on
+sink(x)                 // OK
+----
+
+— but it is not *value-dependent*: a value's qualifier never depends on the runtime value of *other*
+state. That is the property that keeps the analysis a boolean dataflow pass with no solver. (It is
+also the natural boundary at which a deeper, external tool would take over; see _Forward
+compatibility_.)
+
+=== Pass-through methods and polymorphism
+
+Many methods neither taint nor sanitize — `String.trim()`, `.toLowerCase()`, `.substring(int)` — they
+pass taint through. Annotating these `@Untainted` would leak; annotating them `@Tainted` would over-
+report. A third, *polymorphic* qualifier, `@PolyTainted`, expresses "the result's taint matches the
+receiver/argument's taint":
+
+[source,groovy]
+----
+@PolyTainted String trim(@PolyTainted String self) { ... }   // trim(tainted) is tainted; trim(untainted) is untainted
+----
+
+`@PolyTainted` is what makes the model library for the JDK's string API usable. It is part of the
+design but is presented as a refinement: a first cut can model pass-through methods conservatively
+(propagate taint) and add `@PolyTainted` where over-reporting proves painful.
+
+=== Leveraging existing Groovy mechanisms
+
+Groovy already encodes injection safety *structurally*, before any annotation exists, and the checker
+should reward that rather than ignore it. The headline case is `GString`.
+
+`groovy.sql.Sql` special-cases `GString` arguments: a query written as an interpolated string is
+compiled to a parameterized `PreparedStatement`, with the interpolated values bound as parameters
+rather than concatenated into SQL. The *type* of the argument — `GString` versus `String` — already
+distinguishes the safe path from the dangerous one:
+
+[source,groovy]
+----
+sql.execute("SELECT * FROM users WHERE name = ${name}")   // GString  -> parameterized -> SAFE even if name is @Tainted
+sql.execute('SELECT * FROM users WHERE name = ' + name)   // String   -> concatenated  -> ERROR if name is @Tainted
+----
+
+`TaintChecker` recognizes the GString-accepting `Sql` methods as *built-in sanitizing sinks*: the
+interpolated holes become bound parameters, so tainted values inside a GString destined for such a
+sink are safe, while the same data *flattened* into a `String` (via concatenation or `.toString()`)
+and passed to the raw-`String` overload is reported. This generalizes: a `GString` carries its
+structure (fixed fragments versus interpolated values), and APIs that consume that structure
+(parameterized queries, attribute-escaping builders) are inherently safer than their flat-`String`
+equivalents. The checker can be taught these GString-aware APIs out of the box, retrofitting taint
+semantics onto idioms Groovy developers already use — no new annotations on user code required.
+
+This is a distinctively Groovy advantage: the language's structured-string type gives the checker a
+ready-made, zero-annotation safety boundary that a `String`-only language does not have.
+
+=== Defaulting and the soundness / noise trade-off
+
+The default qualifier is `@Untainted`. String and character literals are untainted; unannotated
+locals, parameters, fields, and returns default untainted; the shipped *model library* marks known
+external-input APIs `@Tainted` and known dangerous APIs as `@Untainted`-requiring sinks. The
+consequence is explicit and worth stating plainly:
+
+* *Optimistic default (proposed)* — low false-positive rate; a diagnostic fires only for flows the
+  checker has been told about. The cost is potential *false negatives*: an un-modeled source is
+  treated as untainted and its flows are missed.
+* *Strict mode (flag)* — external inputs (and, optionally, all unannotated boundaries) default
+  `@Tainted`. Sound, but noisier; appropriate for security-critical code that accepts the annotation
+  burden.
+
+The optimistic default is proposed as the out-of-the-box behavior precisely to protect the checker's
+reputation for trustworthiness (see _Risks_).
+
+=== Scope and limitations
+
+* *Intraprocedural plus same-unit interprocedural* — flows are tracked within a method and across
+  calls whose callee is resolvable at compile time (same compilation unit, or modeled in the
+  library). Whole-program, cross-jar taint is out of scope for a type-checker extension.
+* *Dynamic dispatch* — a call whose target cannot be resolved statically yields a loud "flow not
+  tracked" note rather than a silent verdict.
+* *Explicit flows only (initially)* — implicit flows (taint induced by branching on tainted data) are
+  not tracked in the first version; they are a well-known source of noise and are deferred to an
+  opt-in mode. Most real injection bugs are explicit flows.
+* *Collections are coarse* — a container is tracked with a single taint bit rather than per-element;
+  per-element precision is value-dependent and is exactly the boundary handed off to richer tools.
+
+== A worked example — composing with another checker
+
+The value of the checker family is that several run on one compile, each on its own concern:
+
+[source,groovy]
+----
+@TypeChecked(extensions = ['groovy.typecheckers.NullChecker',
+                           'groovy.typecheckers.TaintChecker'])
+class Comments {
+    @Tainted String body(HttpServletRequest req) { req.getParameter('body') }
+
+    void post(Sql sql, Writer page, @NonNull HttpServletRequest req) {
+        @Tainted String raw = body(req)
+        @Untainted String safe = escapeHtml(raw)
+
+        sql.execute("INSERT INTO comments(text) VALUES (${safe})")  // GString sink: parameterized, OK
+        page.write(safe)                                            // untainted to an HTML sink: OK
+    }
+
+    @Untainted String escapeHtml(@Tainted String s) { /* ... */ }
+}
+----
+
+**NullChecker** verifies the request is non-null; **TaintChecker** verifies no tainted comment reaches
+the database or the page. Distinct concerns, distinct error channels, one compile.
+
+== Implementation
+
+`TaintChecker` is a `org.codehaus.groovy.transform.stc.TypeCheckingExtension` (the same SPI as the
+other checkers). During type checking it:
+
+* assigns each expression a taint qualifier, propagating along the dataflow rules above;
+* reads `@Tainted` / `@Untainted` / `@PolyTainted` on declarations, and the shipped model library, to
+  fix qualifiers at boundaries;
+* checks assignments to `@Untainted` targets and arguments at `@Untainted` parameters, reporting a
+  flow that delivers tainted data;
+* recognizes the GString-aware safe sinks (e.g. `groovy.sql.Sql`) as built-in sanitizers.
+
+The non-trivial deliverable is the *model library*: a curated set of qualifier annotations (or external
+stub files) for the JDK and common Groovy/Java libraries — sources (`HttpServletRequest`,
+`System.getenv`, `Files.readString`, `Scanner`, deserialization), sinks (`Statement`,
+`Runtime.exec`/`ProcessBuilder`, `Files`/`Path` constructors, response writers, `MarkupBuilder` raw
+sinks), sanitizers, and `@PolyTainted` pass-through string methods. The accuracy and currency of this
+library, not the propagation engine, is the bulk of the work — and the bulk of the ongoing
+maintenance.
+
+== Prior art
+
+[cols="2,2,2"]
+|===
+|Tool |Approach |Relationship to this proposal
+
+|Checker Framework — Tainting Checker (Java)
+|Pluggable type qualifiers `@Tainted`/`@Untainted`/`@PolyTainted`, compile-time, sound
+|Closest analogue; this proposal adapts the same two-point model to a Groovy `@TypeChecked` extension
+ and adds GString-awareness
+
+|Ballerina taint analysis
+|Built-in `tainted`/`untainted` with compiler-enforced flow
+|Demonstrates a mainstream language shipping taint checking as a first-class, low-ceremony feature
+
+|FindSecBugs / SpotBugs, SonarQube
+|Bytecode/AST heuristics, separate tool
+|Post-hoc and tool-external; this proposal is in-compiler and annotation-directed
+
+|CodeQL, Semgrep
+|Whole-program / query-based dataflow
+|Deeper and cross-jar, but heavyweight and out-of-band; complementary, not a substitute
+
+|OWASP ESAPI, encoder libraries
+|Runtime sanitizers
+|The sanitizers this checker's annotations would mark; the checker enforces that they are *called*
+|===
+
+== Forward compatibility
+
+The `@Tainted` / `@Untainted` qualifiers are the bottom and top of a security lattice. This keeps the
+vocabulary deliberately open-ended:
+
+* *Richer information-flow tools* — an external analysis offering multi-level lattices, value-dependent
+  labels, or proof-backed declassification (for example an SMT-backed verifier such as `groovy-verify`)
+  can consume the *same* `@Tainted` / `@Untainted` annotations as the extremes of a finer ordering.
+  Code annotated for `TaintChecker` therefore remains meaningful to a deeper analysis with no
+  re-annotation, and the two compose cleanly — the cheap checker covers all opted-in code, the deeper
+  tool adds precision and proof where it reaches. This GEP does not depend on any such tool and does
+  not specify their internals; it simply avoids closing the door on them.
+
+* *AI-assisted security* — as automated vulnerability detection and repair mature, the annotation layer
+  is the natural integration point: a tool (or model) proposes source/sink/sanitizer annotations, and
+  the compiler turns those proposals into *checked* guarantees rather than unverified suggestions.
+  Shipping the substrate positions Groovy to gain security posture from that ecosystem as it develops,
+  and to give AI-generated Groovy a compile-time injection backstop.
+
+== Excluded and deferred features
+
+[cols="2,1,3"]
+|===
+|Feature |Status |Rationale
+
+|Implicit (control-flow) taint
+|Deferred (opt-in)
+|A known source of false positives; most injection bugs are explicit flows
+
+|Per-element collection taint
+|Deferred
+|Value-dependent; the boundary at which a richer external tool takes over
+
+|Whole-program / cross-jar tracking
+|Not planned
+|Out of scope for a type-checker extension; CodeQL/Semgrep territory
+
+|Configurable multi-level lattices
+|Not planned (here)
+|A two-point checker needs none; multi-level is left to external tools sharing the vocabulary
+
+|`@PolyTainted` polymorphism
+|Refinement
+|Part of the design; a first cut may model pass-through methods conservatively and add it incrementally
+
+|Strict (sound-by-default) mode
+|Behind a flag
+|Available for security-critical code that accepts the annotation burden
+|===
+
+== Compatibility
+
+=== Backwards compatibility
+
+The feature is entirely opt-in and additive. No existing program changes behavior:
+
+* The checker runs only where `@TypeChecked(extensions = ['groovy.typecheckers.TaintChecker'])` is
+  applied; absent that, nothing changes.
+* `@Tainted` / `@Untainted` are `RUNTIME`-retained marker annotations with no effect on code that does
+  not enable the checker; they are ignored like any unrecognized annotation.
+* The annotations and the checker ship in the existing `groovy-typecheckers` module; core Groovy
+  semantics are untouched.
+
+=== Interaction with other checkers
+
+`TaintChecker` composes with the other extensions on a single `@TypeChecked` declaration, each
+reporting on its own concern through its own diagnostics, as in the worked example above.
+
+== Risks, maintenance, and status
+
+This GEP is deliberately frank about why it is a *placeholder* rather than a committed deliverable.
+
+* *Taint checkers carry a false-positive / maintenance reputation.* Tools in this space are routinely
+  uninstalled because they are too noisy, or quietly distrusted because they are too lax. The proposal
+  leans hard on the low-noise default, GString-awareness, explicit greppable suppression, and loud
+  skips precisely to earn and keep developer trust — but the reputational risk is real and must be
+  managed by conservative defaults and good diagnostics, not wished away.
+
+* *The model library is an ongoing commitment.* The propagation engine is modest; the durable cost is
+  curating and *keeping current* the source/sink/sanitizer annotations for the JDK and the common
+  Groovy/Java ecosystem. This is the part the Groovy team would need to be comfortable owning (or
+  governing as a community-maintained, versioned artifact) before the feature should ship. An
+  out-of-date or incomplete model library is itself a source of both false negatives and false
+  positives.
+
+* *Soundness is a chosen posture, not a guarantee.* The optimistic default is explicitly *unsound* (it
+  can miss un-modeled sources) in exchange for trustworthiness. Users who need soundness opt into
+  strict mode and its noise. The GEP does not claim to prove the absence of injection — only to catch
+  the flows it has been told to look for.
+
+* *Scope is bounded and honest.* Dynamic dispatch, cross-jar flows, implicit flows, and per-element
+  precision are out of scope or deferred; the checker says so rather than pretending otherwise.
+
+Accordingly the status is *Draft (placeholder)*. The near-term purpose is to fix the annotation
+vocabulary (`@Tainted` / `@Untainted`, and the role-by-position convention) so Groovy 6.x can align to
+it, to gather feedback, and to find out whether there is both community appetite and committed
+maintenance for the model library. Implementation in Groovy 7.0 is conditional on that.
+
+== References
+
+* https://checkerframework.org/manual/#tainting-checker[The Checker Framework — Tainting Checker]
+* https://owasp.org/www-project-top-ten/[OWASP Top Ten]
+* https://cwe.mitre.org/data/definitions/89.html[CWE-89: SQL Injection], https://cwe.mitre.org/data/definitions/79.html[CWE-79: Cross-site Scripting], https://cwe.mitre.org/data/definitions/78.html[CWE-78: OS Command Injection]
+* https://ballerina.io/[Ballerina language — taint analysis]
+* https://docs.groovy-lang.org/latest/html/documentation/sql-userguide.html[Groovy `Sql` — GString-based parameterized queries]
+* https://find-sec-bugs.github.io/[FindSecBugs]
+* link:GEP-19.html[GEP-19: Structural Pattern Matching in switch]
+* link:GEP-20.html[GEP-20: Multi-assignment Destructuring]