Add draft threat model + SECURITY.md + AGENTS.md for security-model discoverability#61
Conversation
…l discoverability Adds a draft (v0) threat model plus SECURITY.md and AGENTS.md so an automated scan agent can discover the model via AGENTS.md -> SECURITY.md -> THREAT_MODEL.md. The model is a proposal for the PMC to review; most claims are (inferred) and route to open questions in its section 14. Generated-by: Claude Code (Claude Opus 4.8)
| | Asset serving | classpath/context asset URLs | filesystem/classpath | **Yes (traversal)** | | ||
| | Access whitelisting | `@WhitelistAccessOnly`, `ClientWhitelist`/`LocalhostOnly` | client address | **Yes** | | ||
| | Transport/link security | `RequestSecurityManager`, `LinkSecurity` (HTTP↔HTTPS) | network | **Yes** | | ||
| | Tests / sample apps / docs | `tapestry-core/src/test/app1`, samples | — | No → §3 | |
There was a problem hiding this comment.
Hello, @potiuk ! Why not tapestry-core/src/test (i.e. all internal testing code)? Nothing there ever gets deployed.
thiagohp (PR apache#61): exclude all internal test code (src/test/** across modules, not just tapestry-core/src/test/app1) — none of it deploys. Generalized §2/§3/§11a accordingly. Generated-by: Claude Opus 4.8
|
Thanks Pushed — if anything else on the model looks off, just flag it inline. |
| 1. Is a configured **`tapestry.hmac-passphrase`** effectively **required** (fail-closed / hard warning) so the | ||
| serialized-state deserialization (§8.1) is always HMAC-gated in production? What happens if it is unset? | ||
| *Proposed:* required; unset = startup failure/loud warning, not silent insecure default. |
There was a problem hiding this comment.
Currently, it's a "loud, but non-failure".
A message is logged to error level, and an alert is pushed to the framework’s AlertManager, which will display it on the client-side if the related component/mechanism is used.
From internal discussions, we believe we should keep the loud error during development, but change it to a startup failure if production mode is active.
| 1. Is a configured **`tapestry.hmac-passphrase`** effectively **required** (fail-closed / hard warning) so the | ||
| serialized-state deserialization (§8.1) is always HMAC-gated in production? What happens if it is unset? | ||
| *Proposed:* required; unset = startup failure/loud warning, not silent insecure default. | ||
| 2. Is framework-rendered output **HTML-escaped by default**, with raw output an explicit opt-in? *Proposed:* yes. |
There was a problem hiding this comment.
It's an explicit opt-in, either by using the org.apache.tapestry5.MarkupWriter directly (method named writeRaw), or using the org.apache.tapestry5.corelib.components.OutputRaw component.
If used (internal or external), the developer must know what they are doing and either manually escape or validate beforehand.
Any unchecked passthrough of data in an internal component should be treated as an issue.
| serialized-state deserialization (§8.1) is always HMAC-gated in production? What happens if it is unset? | ||
| *Proposed:* required; unset = startup failure/loud warning, not silent insecure default. | ||
| 2. Is framework-rendered output **HTML-escaped by default**, with raw output an explicit opt-in? *Proposed:* yes. | ||
| 3. Are **assets** protected against path traversal / arbitrary classpath read by default? *Proposed:* yes. |
There was a problem hiding this comment.
Tapestry's rely on a mix of the servlet container’s path normalization and additional, more specific checks of its own, e.g. explicit exclusions of sensitive file extensions.
However, the checks could be improved further.
| 4. Confirm the default **whitelist analyzer** (localhost-only) for `@WhitelistAccessOnly`, and how | ||
| `RequestSecurityManager` decides "secure". *Proposed:* default localhost; secure pages per annotation/config. |
There was a problem hiding this comment.
By default, only org.apache.tapestry5.internal.services.security.LocalhostOnly is contributed, restricting access to localhost or equivalent IPv4 and IPv6 addresses.
However, while checking this code, we found a bug that falsely denies access to certain shortened IPv6 addresses.
See: https://issues.apache.org/jira/browse/TAP5-2832
The RequestSecurityManager decides based on the @Secure annotation on pages if HTTPS is required.
By default, this is only enabled/enforced in production mode, but is configurable via the tapestry.secure-enabled symbol.
| 5. Does Tapestry provide **CSRF** protection for form/event posts, or is that the application's responsibility? | ||
| *Proposed:* application responsibility unless a built-in token exists — confirm. |
There was a problem hiding this comment.
Currently, there's no CSRF module/component available directly in Tapestry.
Only the HMAC provides some form of protection against invalid requests in general, but valid requests are affected by possible CRSF.
So it’s application responsibility until added.
We’re already planning on adding CSRF protection at a later point, but are still working on a concept to integrate it cleanly, flexible, and backward-compatible.
My company has implemented CSRF internally, and there’s https://github.com/porscheinformatik/tapestry-csrf-protection that was primarily developed by a Tapestry committer and a PMC member, so there's prior work to take inspiration from.
…ror not fail-closed (TAP5-2834 planned), escaping default + writeRaw/OutputRaw opt-in, asset traversal via container+own checks, LocalhostOnly + @Secure/secure-enabled (TAP5-2832 IPv6 bug), and no built-in CSRF (app responsibility)
|
Thanks @benweidig — that's a thorough set of answers, folded into
Still open for the PMC when you have a moment: the §11a non-findings list (wave 3, Q6/Q7) — happy to refine it from whatever scanners most often mis-report against Tapestry. (@thiagohp — your test-scope answer is already folded into §3/§11a.) |
| 6. The component categorization you shared earlier — please confirm the §2 family table and the §11a | ||
| non-findings list (especially the "deserialization is HMAC-gated" entry). *Proposed:* per §2/§11a above. |
There was a problem hiding this comment.
LGTM.
Handling invalid/unset/weak HMAC passphrase was mentioned in earlier comment and will be addressed.
Any HMAC bypass should be treated as a finding.
| **Wave 3 — §11a (still with the PMC):** | ||
| 6. The component categorization you shared earlier — please confirm the §2 family table and the §11a | ||
| non-findings list (especially the "deserialization is HMAC-gated" entry). *Proposed:* per §2/§11a above. | ||
| 7. What do scanners most often (re)report that the PMC considers a **non-finding**? (Seeds §11a.) |
There was a problem hiding this comment.
So far, we don't have automatic scanning tools in our CI pipeline.
I've experimented with CodeQL locally, to see if it would make sense to activate it on GitHub.
The findings with the default Java queries were mostly false positives.
Primarily findings based on "user-provided data," like "cross-site scripting" via the output of our JSON module, which doesn't make sense.
|
Thanks @benweidig — much appreciated, and good that the review surfaced the IPv6 LocalhostOnly bug and the HMAC hardening items on your side. Agreed: any HMAC bypass should be treated as a finding, and CSRF being app-responsibility / the path-normalization details are noted. Nothing blocking from our side — merge whenever you're ready and we'll verify discoverability and queue Tapestry. Thanks for the thorough read. |
|
Thanks, @potiuk, for guiding us through! |
This is a draft proposal for the Tapestry PMC to review — please correct, reject, or discuss as needed. Nothing here is a requirement; the maintainers are the decision-makers.
This PR adds
THREAT_MODEL.md+SECURITY.md+AGENTS.md, wiringAGENTS.md -> SECURITY.md -> THREAT_MODEL.md.Framing: Tapestry is a framework — the application developer's pages/components/handlers and the operator's config are trusted, while the web client is the adversary. The most load-bearing mechanism is the HMAC-protected serialized client state: Tapestry round-trips serialized objects through the browser and deserializes them on return, gated by
tapestry.hmac-passphrase(the post-CVE-2021-27850 protection).Draft-first, mostly inferred (~12 documented / 0 maintainer / ~46 inferred); every
*(inferred)*claim routes to a numbered §14 question. The wave-1 rulings:tapestry.hmac-passphraseeffectively required (fail-closed) so the serialized-state deserialization is always HMAC-gated — and what happens if it is unset?I also tried to fold in the component categorization shared earlier — please confirm the §2 family table and the §11a "deserialization is HMAC-gated" non-finding.
Context: the ASF Security team is preparing the project for an automated agentic security scan we're piloting. Drafted via the threat-model-producer rubric. If you'd rather author it yourselves, close this PR and we'll regroup.