blob: f266e7083a410c515421ffa4dec91bb996b44d0a [file]
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
# Machine-readable companion to THREAT_MODEL.md (see its section 14).
# Lets scanning/triage tooling auto-classify findings against Groovy's
# security scope. THREAT_MODEL.md is authoritative; this file mirrors it.
# Section anchors (e.g. "THREAT_MODEL.md#3") are given for every entry.
schema: groovy-threat-model/v1
project: Apache Groovy
document: THREAT_MODEL.md
status: draft-for-pmc-review
applies_to: ["3.0.x", "4.0.x", "5.0.x", "6.0.x-alpha"]
# The single decisive rule. See THREAT_MODEL.md#1 and #4.
core_principle: >-
Groovy is a general-purpose programming and scripting language. Running
the code it is given (filesystem, network, process, reflection) is the
product, not a vulnerability. The trust boundary is the source, scripts,
templates, and objects the embedding application chooses to compile,
evaluate, or deserialize. Groovy is not a sandbox.
core_principle_provenance: maintainer # THREAT_MODEL.md#1 / #4
provenance_tags:
documented: backed by shipped docs/javadoc/source verified in-repo
inferred: reasoned position, not yet confirmed by the PMC
maintainer: confirmed by the PMC through approval of this document
roles: # THREAT_MODEL.md#2
- id: developer
trust: trusted
note: authors source/scripts/templates/classpath/AST-transforms; full JVM authority by design
- id: build-operator
trust: trusted
note: controls what is compiled and the build environment
- id: cli-user
trust: trusted-local
note: runs groovy/groovyc/groovysh/groovyConsole with own privileges on own files
- id: end-user
trust: untrusted
note: supplies DATA to an app built with Groovy; supplies CODE only if the developer chose to evaluate it
# Narrow, default-on conveniences. A default-config violation is VALID.
# THREAT_MODEL.md#8
properties:
- id: P1
title: Parameterized SQL for the GString form
cwe: CWE-89
condition: groovy.sql.Sql GString/bind APIs used (not a hand-concatenated String)
violation: a GString placeholder reaching the driver as literal SQL
severity_if_violated: High
provenance: documented
- id: P2
title: Hardened XML parsing by default
cwe: [CWE-611, CWE-776]
condition: allowDocTypeDeclaration left at default false (XmlParser/XmlSlurper/XmlUtil/FactorySupport)
violation: default-configured parsing resolving an external entity or expanding a DTD bomb
severity_if_violated: High
provenance: documented
- id: P3
title: Data-only structured-data parsing (JSON, YAML, TOML, CSV)
cwe: CWE-502
condition: untyped parse() used (all JsonParserType variants; YAML/TOML/CSV via Jackson untyped binding, YAML uses YamlConverter not SnakeYAML Yaml.load); parseAs(Class,..) binds a caller-supplied type with no default typing
violation: a document causing instantiation of a class it names
severity_if_violated: Critical
scope_note: type-safety guarantee only, not resource-exhaustion; DoS robustness is addressed separately and now depth-bounded by default for all of them, incl. a JsonSlurper nesting-depth cap from 6.0.0 (groovy.json.maxNestingDepth, GROOVY-12064), THREAT_MODEL.md#6; TomlSlurper/CsvSlurper are @Incubating
provenance: documented # verified in groovy-json, groovy-yaml YamlConverter, groovy-toml, groovy-csv
- id: P4
title: Owner-only temporary artifacts
cwe: [CWE-377, CWE-378]
condition: Groovy-created temp dirs via NIO Files.createTempDirectory
violation: a Groovy-created temp artifact being world-readable/writable
severity_if_violated: "Medium (local)"
provenance: documented # CVE-2020-17521 fix class
- id: P5
title: Coordinated security maintenance
condition: supported branches per .github/SECURITY.md
provenance: documented
# Looks like security, is not. THREAT_MODEL.md#9
false_friends:
- SecureASTCustomizer # grammar/AST filter, explicitly not a complete solution; bypassable
- "@ThreadInterrupt" # cooperative runaway-script guard, not a malice boundary
- "@TimedInterrupt"
- "@ConditionInterrupt"
- "@CompileStatic" # correctness/perf, not security
- "@TypeChecked"
- groovy.sql.Sql-non-GString # only GString/bind forms parameterize (P1)
- groovysh / groovyConsole # run with user's full privileges; not sandboxed
not_provided: # THREAT_MODEL.md#9
- a sandbox / isolation of executed code
- protection against malicious source, scripts, templates, or serialized objects
- SecurityManager-based confinement (deprecated/removed in the JDK)
- defense against app-built command/SQL/URL/path/option injection, ReDoS, algorithmic-complexity (hash-flooding) DoS, or unbounded-input DoS
# Trust assumptions about the host/environment. THREAT_MODEL.md#5
environment_assumptions:
- id: jvm-trusted
note: the hosting JVM/JDK is trusted and adequately patched; Groovy inherits its security posture (incl. SecurityManager removal)
provenance: documented
- id: memory-safety
note: Groovy inherits the JVM's memory safety; memory-corruption classes (buffer overflow, use-after-free, native type confusion) are out of model — a JDK/JVM bug, not Groovy's
provenance: documented
- id: release-integrity
note: official releases are GPG-signed/checksummed (ASF process); the build pins deps via Gradle dependency verification (gradle/verification-metadata.xml) and publishes a CycloneDX SBOM per module; jar tampering is a supply-chain compromise, out of model (adversary-not-in-scope; see adversary.cannot_be_assumed_to)
provenance: documented
- id: trusted-config
note: classpath, CompilerConfiguration, AST transforms, extension modules, groovy.* properties reflect operator intent, not an attacker's
provenance: inferred
- id: cli-not-a-privilege-boundary
note: CLI tools run with the user's own privileges; not a boundary between users on a host
provenance: inferred
# THREAT_MODEL.md#7 — bounded adversary (closed capability list).
adversary:
in_scope:
- data supplier: bytes to a parser; values bound as data (SQL params, template bindings not template text); oversized/deeply-nested/adversarial data
- local user on a shared host: reads files subject to OS permissions
capabilities_are_a_closed_list: a finding needing a capability not listed is OUT-OF-MODEL (adversary-not-in-scope)
cannot_be_assumed_to:
- supply code/scripts/templates/serialized objects the app evaluates or deserializes
- alter classpath, AST transforms, extension modules, Grape config, CompilerConfiguration, or groovy.* properties
- replace or tamper with the Groovy artifacts themselves (supply-chain)
- attack the JVM/JDK or third-party dependencies rather than Groovy itself
# Every report resolves to exactly one. THREAT_MODEL.md#13
dispositions:
- id: VALID
meaning: violates a provided property (properties[]) in a default configuration
handling: private report per .github/SECURITY.md; fix + advisory
- id: VALID-HARDENING
meaning: >-
by design and not a CVE, but we choose to improve secure-by-default
behavior anyway — defense-in-depth on a reported issue, or a proactive
hardening recognized as good practice
handling: handled in the open, normal priority; not embargoed
- id: "OUT-OF-MODEL: executes-supplied-code"
meaning: relies on the app compiling/evaluating attacker-influenced code or templates
handling: close citing THREAT_MODEL.md#3
- id: "OUT-OF-MODEL: untrusted-deserialization"
meaning: relies on the app deserializing attacker-controlled bytes
handling: close citing THREAT_MODEL.md#3 / #11
- id: "OUT-OF-MODEL: trusted-input"
meaning: relies on attacker control of classpath, config, AST transforms, or groovy.* properties
handling: close citing THREAT_MODEL.md#3
- id: "OUT-OF-MODEL: downstream-responsibility"
meaning: app-built SQL/command/URL/path from untrusted input
handling: close citing THREAT_MODEL.md#10
- id: "OUT-OF-MODEL: equivalent-harm"
meaning: a new path to an effect the actor could already reach legitimately
handling: close citing THREAT_MODEL.md#3
- id: "OUT-OF-MODEL: adversary-not-in-scope"
meaning: requires a capability outside the section-7 closed list (already-privileged actor, supply-chain tampering)
handling: close citing THREAT_MODEL.md#7
- id: "BY-DESIGN: property-disclaimed"
meaning: targets a not_provided / false_friend item (e.g. SecureASTCustomizer bypass)
handling: close citing THREAT_MODEL.md#9
- id: KNOWN-NON-FINDING
meaning: matches known_non_findings[]
handling: close citing the matched row
- id: MODEL-GAP
meaning: a legitimate in-model scenario this model does not yet cover
handling: update the model, then re-triage
# THREAT_MODEL.md#13 — batched scan reports (one document, many findings).
batch_handling:
rule: assign exactly one disposition per candidate finding
enters_private_flow: [VALID, VALID-HARDENING]
embargo_sensitive: [VALID] # VALID-HARDENING is handled in the open
everything_else: closed in-batch with a pointer to the cited section; not an advisory
# Genuine vulns in explicitly-unstable surface stay VALID and can still get a
# CVE, but are handled at reduced priority. THREAT_MODEL.md#8 / #13.
stability_qualifier:
applies_to:
- "@Incubating APIs (any release stream)"
- pre-release / alpha / beta builds
effect: >-
a VALID or VALID-HARDENING finding confined to this surface keeps its
disposition but is best-effort, reduced-priority, and not embargoed-urgent;
it might be handled in the open in some cases. We do not wash our hands of
it — it can still warrant a CVE.
lifted_when: the same vulnerability also reaches stable, non-incubating surface in a supported release
mirrors: .github/SECURITY.md pre-release "(**)" footnote and the @Incubating note
# Patterns a scanner WILL flag in a language implementation; expected,
# not vulnerabilities unless a concrete in-model data->code/object
# boundary crossing is shown. THREAT_MODEL.md#11a
known_non_findings:
- pattern: GroovyClassLoader / parseClass / defineClass / bytecode generation
disposition: KNOWN-NON-FINDING
rationale: the compiler doing its job
- pattern: reflection, MetaClass, invokeMethod/getProperty/setProperty/methodMissing, MethodHandle/invokedynamic
disposition: KNOWN-NON-FINDING
rationale: core Meta-Object Protocol
- pattern: ProcessGroovyMethods / String.execute() / Runtime.exec wrappers
disposition: "OUT-OF-MODEL: downstream-responsibility"
rationale: intended GDK API; injection needs app-supplied untrusted input
- pattern: runtime types (e.g. Closure and friends) implementing Serializable, readObject/readResolve
disposition: "OUT-OF-MODEL: untrusted-deserialization"
rationale: requires deserializing attacker bytes; apply the capability-vs-data test in serialization_policy (THREAT_MODEL.md#11c) — capability primitives are VALID-HARDENING, arbitrary-graph DoS stays out of model
- pattern: ObjectInputStream use in Groovy's own helpers
disposition: "OUT-OF-MODEL: untrusted-deserialization"
rationale: same as above
- pattern: AST transforms executing code at compile time (local & global)
disposition: KNOWN-NON-FINDING
rationale: by design; compile time is trusted
- pattern: Eval / GroovyShell inside Groovy tools, tests, groovysh, groovyConsole
disposition: "BY-DESIGN: property-disclaimed"
rationale: evaluating code is the product
- pattern: "@Grab / Grape fetching and loading artifacts"
disposition: "OUT-OF-MODEL"
rationale: declared dependency resolution
- pattern: temp-file/dir creation
disposition: KNOWN-NON-FINDING
rationale: NIO owner-only since CVE-2020-17521 (P4)
- pattern: regex (ReDoS), BigInteger/BigDecimal parsing, hash-collision flooding of untrusted map keys
disposition: "OUT-OF-MODEL: downstream-responsibility"
rationale: DoS bounded by developer-chosen input
- pattern: deep recursion / unbounded input in Groovy's OWN data parsers (JsonSlurper, XmlSlurper/XmlParser, groovy-yaml/-toml/-csv)
disposition: VALID-HARDENING
stance_status: maintainer
rationale: >-
robustness of code meant to consume untrusted input; see THREAT_MODEL.md#6. Nesting depth now bounded by
default in all of them — XML mitigated by P2 (secure processing); YAML/TOML/CSV bounded by Jackson
StreamReadConstraints; JsonSlurper enforces a Groovy-level nesting-depth cap (default 1000, matching
StreamReadConstraints; groovy.json.maxNestingDepth / setMaxNestingDepth; GROOVY-12064, 6.0.0). Language
parser excluded (parses trusted source only). No parser-robustness items remain open (see open_items).
- pattern: dynamic proxy / runtime class generation (Proxy, ProxyGenerator)
disposition: KNOWN-NON-FINDING
rationale: core runtime mechanism
- pattern: SecureASTCustomizer bypass demonstrations
disposition: "BY-DESIGN: property-disclaimed"
rationale: it is not a sandbox
stance_status: maintainer; the underlying not-a-boundary fact is documented (its javadoc)
- pattern: Groovy tool/compiler/test output echoing attacker-controlled text, or a dependency emitting agent-targeted text (prompt injection via tool output)
disposition: "OUT-OF-MODEL: downstream-responsibility"
rationale: output is faithful, not sanitized for a downstream sink (shell/browser/SQL/LLM agent); consumer treats it as data; see AGENTS.md data-never-instruction rule
# THREAT_MODEL.md#11b — default-downgrade rules for an automated scanner.
# Downgrade a candidate to out-of-model by default unless its 'unless' also holds.
scanner_calibration:
downgrade_by_default:
- depends_on: evaluating or compiling supplied code/templates
unless: a Groovy component itself feeds untrusted DATA to an evaluator unrequested
- depends_on: deserializing untrusted bytes
unless: a default-config violation of a properties[] entry
- depends_on: equivalent-harm (a new path to authority the actor already holds)
unless: the path crosses a trust boundary the actor did not already hold
- depends_on: malformed-input / DoS against developer-chosen input
unless: the target is one of Groovy's own untrusted-input parsers (VALID-HARDENING)
- depends_on: a malicious dependency, classpath, or groovy.* property
unless: reachability through Groovy's own default-configured code
- depends_on: a PoC the section-7 adversary cannot stage
unless: the capability is within the section-7 closed list
ambiguous: surface as MODEL-GAP for a human; never silently drop
# THREAT_MODEL.md#11c — how serialization findings are dispositioned.
# maintainer-ratified (PMC approval of this document). Baseline: deserializing
# untrusted bytes is the app's risk (out_of_scope) and no per-class change makes
# it safe. Test = capability vs. data.
serialization_policy:
status: maintainer
buckets:
- id: capability-primitive
when: a deserialization path grants a capability beyond reconstructing declared state (method dispatch, class-resolution-by-name, proxy invocation, code exec) AND gating it does not break "restore my data"
disposition: VALID-HARDENING
note: gate it; precedent MethodClosure.ALLOW_RESOLVE (default false) instead of removing Serializable
- id: legitimate-data-serialization
when: round-tripping declared state of POGOs, value types (ranges, tuples, GString), or deliberate closure serialization across a TRUSTED boundary (distributed-compute/caching, both ends owned)
disposition: not-a-finding
note: do not break; no blanket Serializable removal from Closure; if hardened prefer opt-in/transient-by-default for owner/thisObject/delegate (today non-transient) with a rehydration path — an implementation choice, not a scope question
- id: arbitrary-graph-no-primitive
when: DoS/RCE from an arbitrary object graph without naming a specific capability primitive to remove
disposition: "OUT-OF-MODEL: untrusted-deserialization"
note: per-class hardening is whack-a-mole
hinge: bucket capability-primitive vs arbitrary-graph-no-primitive — does the report name a primitive to gate, or just exploit that graphs deserialize? only the former carries a hardening obligation
out_of_scope: # THREAT_MODEL.md#3
- executing — or merely compiling/parsing/statically analyzing — supplied source, scripts, or templates (compile-time metaprogramming/AST transforms run during groovyc, JSR-223 compile, IDE indexing)
- deserializing untrusted data
- absence of a sandbox
- SecureASTCustomizer / AST-filter bypasses
- "@Grab / Grape dependency resolution"
- trusted-input surfaces (classpath, AST transforms, groovy.* properties, ~/.groovy)
- vulnerabilities in the JDK or third-party dependencies (assessed for reachability — a CVE in a part of a dependency Groovy does not use may not affect Groovy)
- embedding-application misuse (hand-built SQL/command strings)
- equivalent-harm (a new path to authority the actor already holds)
- prompt injection aimed at AI agents via dependency/build/test output (supply-chain + agent-toolchain; mitigated by dependency pinning + AGENTS.md data-never-instruction rule)
# THREAT_MODEL.md#3 preamble — out_of_scope is the DEFAULT disposition, not a verdict.
out_of_scope_note: >-
Each report is still assessed for surprising behavior that would catch out a
developer following Groovy's documented secure practices, and can be promoted
to VALID (rarely a CVE), VALID-HARDENING, or MODEL-GAP. When in doubt, report.
misuse_patterns: # THREAT_MODEL.md#11
- untrusted input to Eval/GroovyShell.evaluate/GroovyClassLoader.parseClass/GroovyScriptEngine/JSR-223
- rendering an attacker-controlled template (SimpleTemplateEngine/GStringTemplateEngine/StreamingTemplateEngine/MarkupTemplateEngine/XmlTemplateEngine)
- ObjectInputStream.readObject on untrusted bytes containing Groovy types (crafted graph -> resource exhaustion or historical gadget-chain RCE)
- hand-concatenating untrusted values into a SQL String instead of GString/bind forms
- building a command line from untrusted input and calling .execute()
- option/argument injection — an untrusted value passed as its own argv element (even via the safer String[]/List form) read by the target program as a flag; validate or use a "--" end-of-options separator
- relying on SecureASTCustomizer or @ThreadInterrupt as a sandbox
- enabling @Grab resolution while running untrusted scripts
- parsing untrusted XML after setting allowDocTypeDeclaration = true
# No open items. The former parser-DoS-bounds item was resolved by GROOVY-12064
# (6.0.0): JsonSlurper's nesting-depth cap, captured in properties[P3].scope_note,
# known_non_findings (deep-recursion row), and THREAT_MODEL.md#6.
open_items: []
contacts:
primary: paulk@apache.org
backup: blackdrag@apache.org
report_to:
- security@apache.org
- private@groovy.apache.org