This is an automated email from the ASF dual-hosted git repository.
benweidig pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/tapestry-5.git
The following commit(s) were added to refs/heads/master by this push:
new 291ab2aac Add draft threat model + SECURITY.md + AGENTS.md for
security-model discoverability (#61)
291ab2aac is described below
commit 291ab2aacf6c26c98f77fa013ba3408c9a9f4dd6
Author: Jarek Potiuk <[email protected]>
AuthorDate: Tue Jun 30 07:18:51 2026 +0200
Add draft threat model + SECURITY.md + AGENTS.md for security-model
discoverability (#61)
* Add draft THREAT_MODEL.md + SECURITY.md + AGENTS.md for security-model
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)
* THREAT_MODEL.md: broaden test-code exclusion per PMC review
thiagohp (PR #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
* Fold PMC review answers (PR #61): Q1-Q5 — HMAC-unset is loud-error 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)
---
AGENTS.md | 33 +++++++
SECURITY.md | 14 +++
THREAT_MODEL.md | 280 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++
3 files changed, 327 insertions(+)
diff --git a/AGENTS.md b/AGENTS.md
new file mode 100644
index 000000000..2beb44798
--- /dev/null
+++ b/AGENTS.md
@@ -0,0 +1,33 @@
+<!--
+SPDX-License-Identifier: Apache-2.0
+
+Licensed 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
+
+ https://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.
+-->
+
+# Agent Guide for Apache Tapestry
+
+This file is read by automated agents (security scanners, code analyzers,
+AI assistants) operating on this repository.
+
+## Security
+
+Security model: [SECURITY.md](./SECURITY.md) ->
[THREAT_MODEL.md](./THREAT_MODEL.md)
+
+Agents that scan this repository should consult `SECURITY.md` and the linked
+`THREAT_MODEL.md` before reporting issues. Tapestry is a component-based web
+framework: the untrusted web client is the adversary, while application
+pages/components/handlers and configuration are trusted. The single most
+load-bearing mechanism is the HMAC-protected serialized client state
+(deserialization is gated by `tapestry.hmac-passphrase`); the model also covers
+default output escaping, asset access control, client whitelisting, and
+secure-link enforcement.
diff --git a/SECURITY.md b/SECURITY.md
new file mode 100644
index 000000000..0d8a93824
--- /dev/null
+++ b/SECURITY.md
@@ -0,0 +1,14 @@
+# Security Policy
+
+## Reporting a Vulnerability
+
+Apache Tapestry follows the [Apache Software Foundation security
process](https://www.apache.org/security/).
+Please report suspected vulnerabilities **privately** to `[email protected]`
(the Tapestry PMC is reachable
+at `[email protected]`). Do **not** open public GitHub issues or
pull requests for security reports.
+
+## Threat Model
+
+What Tapestry treats as in/out of scope, the security properties it provides
and disclaims (HMAC-gated
+serialized-state deserialization, default output escaping, asset access
control, client whitelisting,
+secure-link enforcement), the adversary model (the untrusted web client vs.
the trusted application
+developer/operator), and how findings are triaged are documented in
[THREAT_MODEL.md](./THREAT_MODEL.md).
diff --git a/THREAT_MODEL.md b/THREAT_MODEL.md
new file mode 100644
index 000000000..ec90e9535
--- /dev/null
+++ b/THREAT_MODEL.md
@@ -0,0 +1,280 @@
+<!--
+SPDX-License-Identifier: Apache-2.0
+
+Licensed 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
+
+ https://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.
+-->
+
+# Threat Model — Apache Tapestry (5)
+
+## §1 Header
+
+- **Project:** Apache Tapestry — a **component-based Java web application
framework**. Developers build pages
+ and components; Tapestry handles request dispatch, rendering, form/event
processing, asset serving, and
+ serialization of some server-side state into the client (page activation
context, form data) which it
+ validates and deserializes on postback *(documented — README; source
`tapestry-core`, `tapestry-http`)*.
+- **Modelled against:** `apache/tapestry-5` `master`/HEAD (2026-05-31).
+- **Status:** **DRAFT — under Tapestry PMC review** (thiagohp, benweidig;
2026-06); wave-1/2 questions
+ answered and folded. Produced by the ASF Security team via the
`threat-model-producer` rubric
+ (<https://gist.github.com/potiuk/da14a826283038ddfe38cc9fe6310573>).
+- **Reporting / version-binding / legend** as in the sibling models. **Draft
confidence:** ~12 documented /
+ ~10 maintainer / ~36 inferred (wave-1/2 §14 answers folded from PR #61
review). Each *(inferred)* routes to §14.
+
+**Framing note:** Tapestry is a *framework*. The **application developer**
authors pages, components,
+templates, and event handlers — that code is **trusted** (§3). The **untrusted
web client** sending requests,
+form posts, and activation-context URLs is the adversary (§7). The single most
security-load-bearing
+mechanism is the **HMAC-protected serialized client state**: Tapestry
round-trips serialized objects through
+the browser and deserializes them on return, so their integrity rests on a
configured HMAC secret.
+
+## §2 Scope and intended use
+
+Intended use *(documented)*: build and serve a Java web application; clients
interact over HTTP(S) with
+rendered pages, forms, and component events.
+
+Caller roles:
+
+- **Web client (untrusted)** — any browser/agent issuing requests, form posts,
activation-context URLs.
+- **Application developer** — authors pages/components/templates/handlers and
chooses where to use raw output,
+ uploads, whitelisting, HTTPS. **Trusted; out of model as adversary (§3).**
+- **Operator/deployer** — sets `tapestry.hmac-passphrase`, production mode,
and deployment hardening.
+ **Trusted; out of model (§3).**
+
+**Component-family table:**
+
+| Family | Entry point | Touches outside process | In model? |
+| --- | --- | --- | --- |
+| Request dispatch + page activation | URL → page/event, activation context |
— | **Yes** |
+| Serialized client state + **HMAC** | `t:formdata` / activation
serialization, deserialize on postback | **deserialization** | **Yes
(critical)** |
+| Rendering / template output | component render, output escaping | — | **Yes
(XSS)** |
+| Forms + file upload | form submit, multipart upload | fs (temp) | **Yes** |
+| 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 | **all** `src/test/**` across modules (incl.
`tapestry-core/src/test`), samples, docs — none of it deploys | — | No → §3 |
+
+## §3 Out of scope (explicit non-goals)
+
+- **The application developer and operator as adversaries**, and the
application's own page/component/handler
+ code — that is trusted authored code, not an adversary surface (§7)
*(inferred)*.
+- **Misconfiguration** (no HMAC passphrase set, raw output of untrusted data,
exposing a whitelisted page) —
+ Tapestry provides the controls; using them is the developer/operator's job
(§9/§10/§11).
+- **The application's business-logic authorization** beyond the framework's
whitelist/secure-link mechanisms.
+- **All internal test code (`src/test/**` in every module, e.g.
+ `tapestry-core/src/test`), sample apps (`app1`), and documentation** — none
+ of it is deployed, so it is not an adversary surface *(maintainer —
+ thiagohp)*.
+- **The JVM serialization/JCE internals** except as Tapestry selects and uses
them.
+
+## §4 Trust boundaries and data flow
+
+The boundary is the **HTTP request**: parameters, form data, activation
context, and serialized client state
+are untrusted until validated *(inferred)*.
+
+Trust transitions:
+
+1. **URL → page activation/event:** path/query map to a page, an event, and an
activation context. Untrusted
+ context values reach handler parameters *(inferred)*.
+2. **Postback → deserialize serialized state (the critical one):** Tapestry
deserializes the serialized
+ object stream it previously sent to the client. It is accepted **only if
its HMAC verifies** against the
+ configured passphrase — this is what stops an attacker from submitting an
arbitrary serialized object and
+ achieving deserialization RCE *(inferred — `RequestSecurityManager`, HMAC
mechanism; load-bearing, §14)*.
+3. **Render → output:** component output is HTML-escaped by default; raw
output is an explicit developer
+ opt-in *(inferred)*.
+4. **Asset URL → file:** asset requests resolve to classpath/context
resources; path canonicalization must
+ prevent traversal/arbitrary read *(inferred)*.
+5. **Whitelist gate:** `@WhitelistAccessOnly` pages/services are served only
to whitelisted clients (e.g.
+ localhost) *(documented — `ClientWhitelist`, `LocalhostOnly`)*.
+
+**Reachability precondition:** in-model if reachable from an untrusted request
before the framework's
+HMAC/whitelist/escaping controls; a finding requiring the operator to have
left the HMAC passphrase unset or
+the developer to have emitted raw untrusted output is `OUT-OF-MODEL:
non-default-build` / misconfig (§5a/§3).
+
+## §5 Assumptions about the environment
+
+- A servlet container hosting the Tapestry app; a JVM.
+- `tapestry.hmac-passphrase` is configured to a strong secret by the operator
*(inferred — wave-1)*.
+- Production mode disables developer conveniences (detailed exception pages,
component reload) *(inferred)*.
+- TLS is provided by the container; Tapestry's
secure-link/`RequestSecurityManager` enforces HTTPS for pages
+ marked secure *(inferred)*.
+- **What Tapestry does to its host (*(inferred)* — wave-2):** reads
classpath/context assets; writes temp
+ files for uploads; deserializes HMAC-validated client state; not assumed to
open arbitrary sockets or run
+ host commands.
+
+## §5a Build-time and configuration variants
+
+| Knob | Effect | Ruling needed |
+| --- | --- | --- |
+| `tapestry.hmac-passphrase` | Integrity of serialized client state ⇒
deserialization-RCE protection | **Answered (maintainer):** unset is currently
a *loud error* (logged + AlertManager client alert), non-fatal; a startup
failure in production mode is planned
([TAP5-2834](https://issues.apache.org/jira/browse/TAP5-2834)). |
+| Production mode vs. dev | Exposure of stack traces, component source, reload
| Prod disables dev conveniences (§5). |
+| Output escaping default | XSS posture (raw output opt-in) | **Answered
(maintainer):** escaped by default; raw is explicit opt-in via
`MarkupWriter.writeRaw` / `OutputRaw`. |
+| Asset path / `tapestry.asset-path-prefix` + protection |
Traversal/arbitrary-read posture | **Answered (maintainer):** container path
normalization + Tapestry's own sensitive-extension exclusions; could be
improved further. |
+| `@WhitelistAccessOnly` analyzer (default LocalhostOnly) | Who may reach
whitelisted pages | **Answered (maintainer):** default `LocalhostOnly`
(localhost IPv4/IPv6). |
+| Secure-link / `RequestSecurityManager` | HTTPS enforcement for secure pages
| **Answered (maintainer):** decided by the `@Secure` annotation; on by default
in production mode; configurable via `tapestry.secure-enabled`. |
+
+## §6 Assumptions about inputs
+
+| Entry point | Parameter | Attacker-controllable? | Caller/operator must
enforce |
+| --- | --- | --- | --- |
+| page/event request | activation context, event context, query/form params |
**yes** | type coercion; handler validation |
+| postback | `t:formdata` / serialized client state | **yes** | **HMAC
verification before deserialization** |
+| asset request | asset path/URL | **yes** | path canonicalization; no
traversal |
+| file upload | filename, body, content-type | **yes** | size/type limits;
safe temp handling |
+| whitelisted page/service | request origin/address | **yes** | whitelist
analyzer (default localhost) |
+| `tapestry.*` config (hmac passphrase, mode) | all | **no —
operator-trusted** | never sourced from a request |
+
+## §7 Adversary model
+
+- **Primary adversary:** an untrusted web client. Capabilities: submit crafted
activation/event contexts,
+ tampered or replayed serialized client state, malicious asset paths,
oversized/typed uploads, and content
+ intended to reflect as XSS.
+- **Goals:** deserialization RCE via forged client state (defeated only by the
HMAC); arbitrary file read via
+ asset traversal; reach a whitelisted/admin page; stored/reflected XSS; DoS
via large uploads/contexts.
+- **Out of model:** the application developer and operator; anyone holding the
HMAC passphrase or filesystem
+ access.
+
+## §8 Security properties the project provides
+
+*(Conditional on configuration; *(inferred)* pending §14.)*
+
+1. **Serialized-state integrity (deserialization-RCE protection).** Serialized
client state is deserialized
+ only after its **HMAC verifies** against the configured passphrase, so an
attacker cannot submit an
+ arbitrary serialized object *(inferred — load-bearing; the
post-CVE-2021-27850 protection)*. *Symptom:*
+ accepted forged serialized object ⇒ RCE. *Severity:* critical.
+2. **Output escaping by default.** Rendered component output is HTML-escaped;
raw output is an **explicit
+ opt-in** via `MarkupWriter.writeRaw` or the `OutputRaw` component, where
the developer must escape/validate
+ beforehand. (The PMC notes any *unchecked* passthrough of data in an
internal component should itself be
+ treated as a bug.) *(maintainer — benweidig.)* *Symptom:* reflected/stored
XSS from framework-rendered
+ values. *Severity:* high–critical.
+3. **Asset access control.** Asset URLs resolve only to intended
classpath/context resources; traversal is
+ prevented by a mix of the servlet container's path normalization and
Tapestry's own checks (e.g. explicit
+ exclusion of sensitive file extensions). The PMC notes these checks could
be improved further. *(maintainer
+ — benweidig.)* *Symptom:* read of files outside the asset roots.
*Severity:* critical.
+4. **Whitelist enforcement.** `@WhitelistAccessOnly` resources are served only
to whitelisted clients; by
+ default only `LocalhostOnly` is contributed (localhost IPv4/IPv6)
*(maintainer — benweidig)*. *Symptom:* a
+ whitelisted page reachable by a non-whitelisted client. *Severity:* high.
(The PMC noted a correctness bug
+ where certain shortened IPv6 addresses are falsely *denied* —
over-restrictive, not a bypass:
+ [TAP5-2832](https://issues.apache.org/jira/browse/TAP5-2832).)
+5. **Secure-link / HTTPS enforcement.** `RequestSecurityManager` decides
whether HTTPS is required from a
+ page's `@Secure` annotation; enforcement is on by default in **production
mode** and configurable via the
+ `tapestry.secure-enabled` symbol *(maintainer — benweidig)*. *Symptom:*
secure page served over plain HTTP.
+ *Severity:* medium–high.
+
+## §9 Security properties the project does NOT provide
+
+- **No serialized-state protection without a configured HMAC passphrase** — if
unset/weak, §8.1 is void and the
+ deserialization surface reopens. An unset passphrase is currently a *loud
error* (logged + an AlertManager
+ client-side alert), **not yet a hard startup failure**; making it
fail-closed in production mode is planned
+ ([TAP5-2834](https://issues.apache.org/jira/browse/TAP5-2834)) *(maintainer
— benweidig)*.
+- **No built-in CSRF protection.** Tapestry ships no CSRF module/component.
The HMAC guards *invalid* requests,
+ but a *valid* request is still subject to CSRF, so CSRF defence is the
**application's responsibility** until
+ a built-in lands (the PMC is planning one; prior art:
+ <https://github.com/porscheinformatik/tapestry-csrf-protection>, primarily
by a Tapestry committer/PMC
+ member) *(maintainer — benweidig)*.
+- **No XSS protection for developer-emitted raw output** — `OutputRaw`/raw
markup of untrusted data is the
+ developer's responsibility (§10).
+- **No defence against the application developer or operator** (§3).
+- **No application-level authorization** beyond the whitelist/secure-link
mechanisms; page-level access control
+ for normal pages is the app's job.
+
+**False friends:**
+
+- *The serialized client state looks like opaque framework plumbing but is an
attacker-reachable
+ deserialization channel* — its safety is entirely the HMAC; protect the
passphrase like a key.
+- *Whitelist "access only" looks like authentication but is an address/origin
filter* (default localhost), not
+ user authn.
+- *Default escaping protects framework-rendered values, not raw output a
component deliberately emits.*
+
+**Well-known attack classes to keep in view:** Java **deserialization** (gated
by the HMAC); **XSS** via raw
+output; **path traversal** via asset URLs; **CSRF** on form/event posts;
**open redirect** via link/redirect
+parameters; upload-based DoS / content-type confusion.
+
+## §10 Downstream (developer/operator) responsibilities
+
+- **Set a strong `tapestry.hmac-passphrase`** and protect it; treat it as a
cryptographic key.
+- Run in **production mode**; never expose dev exception pages / component
sources publicly.
+- Never emit **raw (unescaped) output** of untrusted data; rely on default
escaping otherwise.
+- Use `@WhitelistAccessOnly` for admin/diagnostic pages and confirm the
whitelist analyzer fits the
+ deployment (default localhost).
+- Mark sensitive pages secure (HTTPS) and run behind TLS; set upload size/type
limits.
+
+## §11 Known misuse patterns
+
+- Deploying without configuring `tapestry.hmac-passphrase` (reopens the
deserialization surface).
+- Emitting untrusted data as raw markup (XSS).
+- Running in development mode in production (information disclosure).
+- Exposing diagnostic/whitelisted pages to the public.
+- Trusting activation/event context values without validation in handlers.
+
+## §11a Known non-findings (recurring false positives)
+
+*(v0 seed — the PMC will own the authoritative list — §14.)*
+
+- **"Java deserialization in Tapestry"** reports that ignore the **HMAC gate**
— `KNOWN-NON-FINDING` when the
+ HMAC verification is in place (the post-CVE-2021-27850 design); only an HMAC
bypass or unset passphrase is `VALID`.
+- **XSS attributed to a developer's raw output** — developer responsibility
(§9/§10), not a framework default.
+- **Findings in any `src/test/**` (internal test code, e.g.
`tapestry-core/src/test`) / samples / docs** — out of scope (§3).
+- **Whitelisted page reachable from localhost** — by design (§8.4).
+- **Dev-mode information disclosure** against a dev configuration — operator
posture (§5a/§11).
+
+## §12 Conditions that would change this model
+
+- A change to the HMAC/serialized-state protection or default passphrase
handling.
+- A change to default output escaping or asset path protection.
+- A new client-reachable serialization/deserialization path.
+- A change to the whitelist analyzer default or secure-link defaults.
+- Any report not cleanly routable to a §13 disposition.
+
+## §13 Triage dispositions
+
+| Disposition | Meaning | Licensed by |
+| --- | --- | --- |
+| `VALID` | Violates a claimed property via an in-scope adversary/input in a
default/secure config. | §8, §6, §7 |
+| `VALID-HARDENING` | No §8 property broken, but a §11 misuse warrants a safer
default/guard. | §11 |
+| `OUT-OF-MODEL: trusted-input` | Requires control of app code / config / HMAC
passphrase. | §6, §3 |
+| `OUT-OF-MODEL: adversary-not-in-scope` | Requires developer/operator/key
capability. | §7, §3 |
+| `OUT-OF-MODEL: unsupported-component` | Lands in tests, sample apps, docs. |
§3 |
+| `OUT-OF-MODEL: non-default-build` | Only when the HMAC passphrase was unset
or raw output / dev mode used. | §5a |
+| `BY-DESIGN: property-disclaimed` | Concerns a §9-disclaimed property (raw
output; whitelist ≠ authn). | §9 |
+| `KNOWN-NON-FINDING` | Matches a §11a entry. | §11a |
+| `MODEL-GAP` | Routes to none of the above → revise the model. | §12 |
+
+## §14 Open questions for the maintainers
+
+**Wave 1 — the deserialization gate + defaults (§5a/§8) — answered (benweidig,
PR #61):**
+1. **HMAC passphrase when unset — answered.** Not currently fail-closed: an
unset passphrase is a *loud error*
+ (logged + an AlertManager client-side alert), non-fatal. A startup failure
in production mode is planned
+ ([TAP5-2834](https://issues.apache.org/jira/browse/TAP5-2834)). Folded into
§5a / §8.1 / §9.
+2. **Output escaping — answered.** Escaped by default; raw output is an
explicit opt-in via
+ `MarkupWriter.writeRaw` / the `OutputRaw` component, where the developer
must escape/validate. Folded into §8.2.
+3. **Asset traversal — answered.** Mitigated by the servlet container's path
normalization plus Tapestry's own
+ checks (explicit sensitive-extension exclusions); "could be improved
further." Folded into §8.3.
+
+**Wave 2 — whitelist, secure-link, CSRF (§8/§9) — answered (benweidig, PR
#61):**
+4. **Whitelist + secure-link — answered.** Default analyzer is `LocalhostOnly`
(localhost IPv4/IPv6; note the
+ over-restrictive IPv6 bug
[TAP5-2832](https://issues.apache.org/jira/browse/TAP5-2832)).
+ `RequestSecurityManager` requires HTTPS based on the `@Secure` annotation,
enforced in production mode by
+ default and configurable via `tapestry.secure-enabled`. Folded into §8.4 /
§8.5.
+5. **CSRF — answered.** No built-in CSRF protection; it is the application's
responsibility until one lands
+ (planned; prior art porscheinformatik/tapestry-csrf-protection). Folded
into §9.
+
+**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.)
+
+**Meta:**
+8. Confirm this model lives as root `THREAT_MODEL.md` referenced from a new
`SECURITY.md`. *Proposed:* yes.
+
+## §15 Machine-readable companion
+
+Deferred for v0; a `threat-model.yaml` can later encode the §6 trust table,
§2/§3 scoping, §8 rows, §9 false
+friends, §11a non-findings, and §13 dispositions.