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

lukaszlenart pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/struts.git


The following commit(s) were added to refs/heads/main by this push:
     new 36e169fc0 docs(skills): require the request-to-sink path in every 
security triage
36e169fc0 is described below

commit 36e169fc028d7feeaea3b44ee9fa8bc996d2c514
Author: Lukasz Lenart <[email protected]>
AuthorDate: Thu Nov 5 11:04:22 2026 +0100

    docs(skills): require the request-to-sink path in every security triage
    
    The triaging-security-reports skill verified the mechanism a report
    names but let the assessment stop there. In two triages this week the
    sink was confirmed by a direct probe while the question that decides
    ownership - does framework code carry the request value into that
    sink, or does the application hand it over - was answered from memory
    or not at all.
    
    The assessment now carries a path table, one row per hop from the HTTP
    request to the sink, each row a file:line read this session, a probe
    executed, or the literal "not traced". The table decides ownership:
    input the framework accepts and operates on (type conversion, parsing,
    tag formatting) is the framework's fault even when the developer could
    have validated it afterwards; input the developer hands to a framework
    evaluation capability (findValue, getText, template %{}) is the
    application's.
    
    Baseline: four subagent runs against the unedited skill. The converter
    scenario traced the path unprompted in both runs. The OGNL-gadget
    scenario traced the parameter hops but left the report's second channel
    (tag reflection) untouched and deferred the remaining channels to a
    memory note; the first-pass triage in the live session traced nothing.
    
    Co-Authored-By: Claude Mythos 5.1 <[email protected]>
---
 .claude/skills/triaging-security-reports/SKILL.md | 43 +++++++++++++++++++++--
 1 file changed, 41 insertions(+), 2 deletions(-)

diff --git a/.claude/skills/triaging-security-reports/SKILL.md 
b/.claude/skills/triaging-security-reports/SKILL.md
index 1b1df2a3e..cc5abe5ca 100644
--- a/.claude/skills/triaging-security-reports/SKILL.md
+++ b/.claude/skills/triaging-security-reports/SKILL.md
@@ -1,6 +1,6 @@
 ---
 name: triaging-security-reports
-description: Use when a vulnerability or security report arrives for triage, 
when assessing a CVE/RCE/OGNL/injection claim against the code, or when 
drafting a reply to a security researcher — to research the claim from source 
without trusting the reporter and without fabricating your own facts.
+description: Use when a vulnerability or security report arrives for triage, 
when assessing a CVE/RCE/OGNL/injection claim against the code, when deciding 
whether a finding is the framework's fault or the application's, or when 
drafting a reply to a security researcher — to research the claim from source 
without trusting the reporter and without fabricating your own facts.
 ---
 
 # Triaging Security Reports
@@ -59,13 +59,47 @@ struts.parameters.requireAnnotations=true     # 
default.properties OVERRIDES it
 
 **The effective default is `true`.** Always trace the full chain: field 
initializer → `@Inject` setter → `default.properties` → any struts.xml 
override. State the *effective runtime* value, and cite the file that actually 
wins.
 
+## Trace the path from the request to the sink
+
+A sink that fires is a *mechanism*. It becomes a framework finding only when 
**framework code carries
+the untrusted input to it**. Every assessment therefore contains a path table, 
walked this session,
+from the HTTP request to the operation the report names:
+
+| Hop | What carries the input | file:line read / test executed |
+|---|---|---|
+| request → framework entry | e.g. `Dispatcher`, a mapper, a multipart parser 
| … |
+| framework gate(s) | name/value checks, converters, allowlists | … |
+| framework → sink | the call that performs the dangerous operation | … |
+
+Every hop is either a file:line you read, a probe you ran, or the literal 
words **"not traced"**. A
+hop covered only by a memory note, an earlier triage, or the reporter's own 
description is "not
+traced". When the report names a second channel ("also reflected via tags", 
"also via cookies"), that
+channel gets its own row.
+
+The table decides ownership, which is the verdict's first sentence:
+
+- **The framework accepts the input and operates on it** — a request parameter 
bound through type
+  conversion, a header the framework parses, a body the framework reads, a 
value a framework tag
+  formats. The developer only declared a type or used a tag as documented. 
**Framework fault**,
+  whatever the developer could have validated afterwards.
+- **The developer hands the input to a framework evaluation capability** — 
`findValue(request
+  string)`, `getText(request string)`, a template-authored `%{}` around a 
request value, an action
+  that moves an upload to the web root. **Application fault.** State the 
boundary and which hop the
+  application owns.
+- **Both**: the framework-owned part (a deny-list gap, a missing bound) is 
still a framework finding
+  at its own severity, usually hardening; the application-owned part is not.
+
+Prefer an executed walk: push the payload through the real entry point 
(`ParametersInterceptor`,
+the tag, the converter) beside a direct call to the sink. Same string, two 
routes, one fires — that
+differential is the ownership evidence.
+
 ## Vulnerability vs. operator responsibility
 
 "In the default configuration" is a crutch — drop it. Decide the real question:
 
 - **Is it a vulnerability?** Then it's a vulnerability whether or not it's the 
default. Handle it privately per `SECURITY.md`.
 - **Does it require an operator to opt into an insecure configuration?** A 
documented, opt-in setting (e.g. `cookiesName=*`, `devMode=true`) that works as 
advertised is the operator's responsibility, provided the docs carry the 
warning. Say "X works as documented; the operator owns the security 
implications of enabling it" — not "not a vuln *in the default config*."
-- **Is the RCE/escalation only reachable via application code the framework 
can't constrain?** (e.g. an action that moves an uploaded file to a web root.) 
Then it's an application concern, not a framework vulnerability — state that 
boundary explicitly.
+- **Is the sink only reachable through application code?** Then the path table 
says so, and the ownership rule above applies — state that boundary explicitly.
 
 ## Drafting the reply — only when you are asked
 
@@ -107,6 +141,8 @@ Once you *have* been asked:
 - About to state a "default" from a field initializer — did you check 
`default.properties`?
 - Citing the reporter's line number without having traced its call path.
 - Asserting "no gate / no check exists" without having grepped for it.
+- The sink fires in a direct probe and you are about to write the verdict — 
where is the path table? A mechanism without a request path is not yet a 
finding.
+- About to confirm an earlier verdict, or fill a path hop, from a memory note 
— that hop is "not traced" until you read it this session.
 - Two of your own claims contradict each other → at least one is unverified. 
Stop and verify both.
 - Promising a fix/warning "we'll add" without checking it isn't already there.
 - Writing "not a vulnerability in the default configuration" → reframe as 
vuln-or-not + operator responsibility.
@@ -121,6 +157,9 @@ Once you *have* been asked:
 | "The field defaults to false, so the gate is off by default" | 
`default.properties` may override it to true. Check the effective value. |
 | "I'll add a mitigation to strengthen the rejection" | An unverified 
mitigation that's wrong discredits the whole response. Verify or omit. |
 | "It rejects the payload, obviously" | Confirm the specific PoC string fails 
the specific filter (e.g. full-match regex `ACCEPTED_PATTERN`). |
+| "The gadget fires, so the report is confirmed" | The gadget is the 
mechanism. The verdict needs the path: does framework code carry a request 
value into it? |
+| "The developer declared the type / used the tag, so it's their problem" | 
Declaring a `BigDecimal` property or rendering a value with `<s:property>` is 
documented use. The framework does the parsing or formatting; the fault is the 
framework's. |
+| "The earlier triage already walked the request channels" | Then it is quick 
to walk again. A hop you did not read this session goes in the table as "not 
traced". |
 | "We should add a startup warning" | Grep first — the warning frequently 
already exists. |
 | "Not a vuln in default config" | Either it's a vuln or it's operator-owned 
opt-in. The default-config hedge muddies both. |
 | "Triage is done, so drafting the reply is the next step" | Triage ends at 
the assessment. Replying is a separate task the user starts. |

Reply via email to