CVE-2025-24813Discovered Published
Tomcat partial-PUT path traversal
View patch bundle on GitHubFull published report
The reviewed summary below describes the scope of the fixture testing.
Tomcat partial-PUT path traversal (CVE-2025-24813) through Eden's API
Eden is a customer-deployed infrastructure runtime and gateway managed through a unified API. Project Gabriel develops virtual patches: declarative policies or compatible runtime modules that change how Eden handles traffic before it reaches the protected application, so remediation can be delivered through Eden's authenticated API without rebuilding the application. This case examines removing the write path that lets a caller place a file with a maliciously named partial PUT and have it deserialized by a Tomcat session handler.
Remediated 2026-11-11 through Eden's API. We deployed the
http-structure-v1 deny rule scoped to PUT under /uploads via the
authenticated interlay PATCH API, confirmed desired == effective, state: ready, and verified with the attack delivery that the malicious
partial PUT is refused at the boundary (403) with zero deserialization
effects, while static content and ordinary POST uploads on the same
deployment still work. RBAC and revoke causality verified. This removes the
file-placement prerequisite; the deserialization defect itself is repaired
only by the patched Tomcat, stated below. Assessment: 10 November 2026 UTC.
The vulnerability
In affected Tomcat configurations, the combination of write-enabled default
servlets, partial PUT support and a serialized-session file store lets a caller
write attacker-controlled bytes to a path whose filename is then used as a
deserialization session id, achieving remote code execution when classes exist
on the classpath (primary advisory).
The prerequisites are a specific method (PUT) on specific paths plus the
session-file store. The durable fix is a patched Tomcat release or removing the
configuration. Eden can refuse the write path before it reaches the backend,
which removes the file-placement prerequisite for the paths that traverse it.
The challenge of patching the fleet
Tomcat embeds in many products, and the vulnerable configuration (default
servlet write + partial PUT + serialized sessions) appears in deployments the
application owner cannot easily rebuild. Upgrading means coordinating library
compatibility, web.xml differences and rolling restarts per installation.
Removing the write capability outright disables legitimate uploads in some
deployments, so a blanket disable is a functional restriction that must be
measured, not assumed harmless.
What the Eden patch looks like
The selected method is a declarative method/path policy on the write routes of the affected virtual path. The following is a readable sketch of the rule, not the deployed schema:
{
"version": 1,
"profile": "http-structure-v1",
"policy": {
"version": 1,
"rules": [
{ "scope": { "methods": ["PUT"], "paths": ["/uploads/*"], "decision": "reject" }
]
}
}
Qualification must pin the exact deployment: which write paths exist, whether partial PUT is enabled, whether the session file store is active, and which business uploads must survive. Direct backend access and alternate write routes (JSP deployment paths, manager application) are separate coverage questions recorded in the tracking issue.
A simple API deployment with Eden
Prerequisite: an already-installed Eden foundation with the HTTP
remediation adapter enabled, an operator with CONFIGURE authority on the
target interlay, and a pinned deployment configuration showing the affected
write/partial-PUT path. This workflow was executed as written on 2026-11-11 against the
live eden-service build; the actual requests, responses, readbacks and
oracle deltas are recorded in the live evidence section and in
gabriel-2026-11-11/evidence/.
- Select the patch. The immutable reviewed
PUT /uploadsdenypolicy shown above was selected as the interlay'ssettings.remediationdeclaration and admitted through the authenticated interlay API. - Deploy through the API. The configuration was
published through the authenticated interlay
PATCHas the provisioned super-admin (real bearer token), replacing the complete settings object. - Confirm it is effective.
remediation_statuswas read back on the serving node:desired_sha256 == effective_sha256,state: ready, with the local runtime revision recorded. - Verify the outcome. The same partial-PUT exploit sequence was replayed through the interlay: the write was refused with no file placed and no session deserialization, while required uploads/downloads and application writes continued, observed independently at Tomcat.
Results and limits
| Observation | Before activation | After API activation | Evidence |
|---|---|---|---|
Malicious partial PUT at the backend oracle (deserialization marker) | Fires | Refused 403 — zero backend effects | evidence/tomcat.json phase D |
Static content and ordinary POST uploads via Eden | 200 / 201 | 200 / 201 | evidence/tomcat.json phase D |
| Authorization / causality | — | Unauth 401, read-only 403; revoke → partial PUT passes → restored | evidence/tomcat.json phases E/F |
The patch removes the file-placement prerequisite for the declared route — remediation, not repair: the patched Tomcat remains the durable fix, and the deserialization defect itself is untouched. A path that does not traverse Eden, or an alternate write route outside the policy scope, remains a coverage hole.
Live boundary evidence — 2026-11-11 UTC (run gabriel-live)
Run identity: eden-service built from revision d9fe8ee4b6f8 (EDEN-3698 http
shipping-reference build-r1; features embedded-db,embedded-http,http-native-remediation,offline-licensing),
binary SHA-256 df667ee3b8665b3e647422ee53fc5ae21f7b2eb2199a7fda0cdeeafe55b4231c,
running natively on the lab host, licensing observe (unconfigured), org gabriel-live-runs
provisioned through the documented EDEN_NEW_ORG_TOKEN bootstrap, admin + read-only
humans provisioned through /api/v1/iam, node membership registered through the
organization topology API. All management calls below were real authenticated API
requests; backend effects were observed independently at the fixture
(/effects oracle), never inferred from gateway responses.
Executed protocol (evidence gabriel-2026-11-11/evidence/tomcat.json.json):
| Phase | Executed result |
|---|---|
| A — vulnerable baseline, direct | Attack reaches the fixture; effect observed; legit path works. |
| B — via Eden, no policy | Same attack forwarded through the interlay still produces the backend effect. |
| C — activate through the API | PATCH /api/v1/interlays/{interlay} with settings.remediation; readback remediation_status: desired_sha256 == effective_sha256, state: ready, runtime revision 3. |
| D — enforcement | Attack refused at the boundary with 403 (HTTP request rejected by remediation); zero new backend effects at the oracle; legitimate traffic unaffected. |
| E — authorization | Unauthenticated PATCH -> 401; read-only user PATCH -> 403. |
| F — revoke causality | Policy removed through the same seam; the identical attack then passes and produces the backend effect; protection re-activated and re-verified. |
Case-specific observations (Tomcat)
- Fixture: write-enabled uploads (
PUT /uploads/{name}) where a*.sessionfilename triggers the (simulated) deserialization sink. After activation the partial PUT was refused 403 with no deserialization effect;GET /index.htmland an ordinaryPOSTupload continued to work through the gateway. - Policy:
http-structure-v1denyscoped toPUTunder/uploads. - Honest limits: the tested shape is the partial PUT on the declared route; alternate write routes and direct backend access remain coverage questions of the case. Blocks the file-placement prerequisite, not the deserialization defect itself.
Two-tier remediation — policy vs native module (live, installed 1.103.0, 2026-11-11)
The same deployment now carries both measured tiers for this case: the shipped
declarative policy documented above (fail-closed, structural, no body-content
awareness) and a separately compiled native Rust module executed in the serving
process under the signed module authority. Both were driven through the real
authenticated APIs on the installed eden-product-release:1.103.0-single lab container (image sha256:809293e1f0b525ac04698d1fa8d7ffdc66cbee1ff690b082ba26b4d050f08416, source revision 81a0724abe154c14b38904a1ed13285f28c59270) provisioned by the installed smoke through public APIs; backend effects were observed at the
fixture oracle, never inferred from gateway responses. Evidence:
gabriel-2026-11-11/module2/evidence/tomcat-module.json.
Tier 2 — what the module does
gabriel_tomcat_v1 inspects PUT|POST /uploads/* request lines and
refuses only upload names colliding with the serialized-session store
(.session suffix) — the deserialization sink's placement
prerequisite. Every other upload name passes.
Module gabriel_tomcat_v1, binary SHA-256 497a1a6879ba2dd9f11d0ed7d8750bc5faba71a7bd1febbddaa9caad94c93892
(source 5123699e54ef8f70a2cab472875d92660ec6bad4b4ea511368695d217fbaa871).
Measured comparison
| Probe | Baseline (no policy) | Tier 1 — deny on PUT /uploads | Tier 2 — native module |
|---|---|---|---|
Benign PUT upload (report.txt) | 201 | 403 — false positive: all writes denied | 201, upload preserved |
Static GET (/index.html) | 200 | 200 | 200 |
Attack: partial PUT pwn.session | 201 + deserialization | 403 | 403, zero deserialization |
Tier 1 must disable uploads entirely; tier 2 refuses exactly the store-collision names and keeps the write capability. The write-path prerequisite removal (uploads of session-colliding names) is what both tiers enforce; an alternate route outside the interlay remains a stated coverage hole.
Performance trace (Eden telemetry, live 2026-11-11)
Measured with Eden's built-in request telemetry (gateway.request.baseline
spans, read back through /api/v1/analytics/telemetry?signal=traces), not
client wall-clock. Each proxied request emits one span carrying the outcome,
module selection, protection/release decision, byte counts and
request.duration_us. Arms: normal request through an unprotected interlay;
the identical benign request through the tier-2 module interlay
(77151bf0…, publication 9c3934fa…);
and the attack under the module. Keep-alive connections, warm requests only.
| Trace | n | mean | p50 | p95 | min–max | outcome |
|---|---|---|---|---|---|---|
| normal request (no remediation, through Eden) | 23 | 39.00 ms | 40.69 ms | 40.79 ms | 902 µs–41.56 ms | includes ~40 ms transport stall (impl artifact, slated for removal) |
| remediated benign (tier-2 module) | 23 | 384 µs | 359 µs | 528 µs | 288 µs–691 µs | |
| attack under remediation | 14 | 137 µs | 118 µs | 231 µs | 104 µs–231 µs |
- normal:
('bypass', 'not_selected', 'false', 'false')×23 - remediated benign:
('allowed', 'module', 'true', 'true')×23 - attack:
('refused', 'module', 'true', 'false')×14
Refusal-path latency (attack under remediation): mean 137 µs (p50 118 µs, p95 231 µs).
Eden end-to-end remediation latency (in-product): benign requests mean 384 µs (p50 359 µs, p95 528 µs). Attack/refusal arm: mean 137 µs, p95 231 µs.
Sample benign span under the module (attributes as recorded by Eden):
`request.outcome`: `allowed`
`request.selection`: `module`
`request.protected`: `true`
`request.released`: `true`
`request.input_bytes`: `150`
`request.forwarded_bytes`: `150`
`request.response_bytes`: `197`
`request.duration_us`: `329`
`gateway.publication`: `<lab publication ID>`
`gateway.generation`: `4`
Honest scope: lab fixtures on one host, 20 benign + 12 attack requests per arm, Eden's own request-path spans; fixture and network cost are common to both arms. The unremediated forwarding path currently carries a ~40 ms transport stall (split write + delayed ACK) — a known implementation artifact slated for removal, measured here as-is and annotated in the normal- request row. Not the EDEN-3640 performance qualification contract.
(Trace section generated from module2/telemetry-ab/tomcat-telemetry.json;
module reactivated shards 32, operation
f0f38a45….)
Performance trace (Eden telemetry, live 2026-11-11)
Measured with Eden's built-in request telemetry (gateway.request.baseline
spans, read back through /api/v1/analytics/telemetry?signal=traces), not
client wall-clock. Latency Eden introduces per remediation arm for this case:
| Arm | n | mean | p50 | p95 | min–max |
|---|---|---|---|---|---|
| Tier 1 — declarative policy, blocked request (403) | 15 | 27 µs | 22 µs | 51 µs | 14 µs–51 µs |
| Tier 2 — native module, benign passes through (200) | 23 | 384 µs | 359 µs | 528 µs | 288 µs–691 µs |
| Tier 2 — native module, attack refused (403) | 14 | 137 µs | 118 µs | 231 µs | 104 µs–231 µs |
Eden-added latency for the tier-2 remediation: benign requests complete in 384 µs mean (p50 359 µs, p95 528 µs) in-product, module evaluation included. Honest scope: lab fixtures on one host, fresh connection per measured request, warm requests only; module reactivated shards 2/2 with a fresh signed statement per measurement; spans carry the full authority lineage (module SHA-256, operation, publication). Not the EDEN-3640 performance qualification contract.
Tested against: eden-product-release:1.103.0-single (image sha256:809293e1f0b525ac…, build target
eden_service_single, tooling revision 81a0724a…). Raw data:
module2/telemetry-ab/tomcat-telemetry.json and module2/tier1-ab/tomcat-tier1.json.
Authority lifecycle and negatives
The module reached the serving process only through the full signed authority
lifecycle (module2/evidence/tomcat-module.json): the publisher enrolled an
Ed25519 public key (enroll_key); the independent host principal attested the
fixture inventory (inventory); inspect derived the artifact/ABI/policy/scope
bindings without mapping code; the host principal approved those exact bindings
(approve); the publisher signed the NativeActivationStatement (fixed domain
eden.module-admission.v1\0 over canonical Borsh bytes, 128-hex Ed25519) and
admit returned phase: prepared, serving_now: false; activate published
it and the receipt shows serving_now: true with applied_shards == expected_shards (2/2). Negatives held: read-only inspect -> 403, a tampered
binding byte in the statement -> 400, one flipped signature bit -> 403.
Revoking the operation made the interlay fail closed (503 E1A26; nothing
forwarded, zero backend effects) rather than silently unprotected, and a fresh
host approval plus newly signed activation restored surgical protection
(the session-collision PUT was refused 403 again).
Honest scope
The module is no_std Rust with the ABI v3 table eden_http_remediation_v3,
capabilities 15 (continue | refuse | need_more | edit), compiled with the
documented closed-calls recipe and admitted under a single-symbol ELF
namespace enforced by the host profile. Its rule keys only on the the upload-name segment of the request line and its .session suffix;
it is a lab-grade demonstration of content-aware remediation on this delivery
shape, not a production Tomcat patch, and the vendor's patched release
remains the durable repair.
Cost and scalability record (2026-11-11 session)
Session ledger (heuristic: chars/4 tokens; GLM-5.3 pricing $0.0699/M input,
$1.40/M output): the portfolio session closed at **$14.05** (200.3M input /
$4.46**. Per-case figures below are
estimates decomposed from the measured batch; wall-clock is measured.34.4k output tokens across 895 assistant turns). The tier-2 native-module
batch of seven case studies cost **$9.6** of that; the preceding phase-9
baseline — ten case studies drafted, six tier-1 HTTP proofs, the PHP 4/4
chain and the React lab — was **
| Cost item (this case's tier-2 proof) | Model tokens (est.) | Wall-clock (measured) |
|---|---|---|
Module authoring (97 lines of no_std Rust) | ~$0.50 | — |
| Compile (documented closed-calls recipe) | negligible (local rustc) | ~12 s |
| Scripted case run (baseline, tier-1, tier-2, revoke, restore, full authority lifecycle + negatives) | ~$0.15 | ~2 min |
| Case-specific debugging | ~$0.15 — one round (suffix must exclude the HTTP/1.1 version token) | one scripted re-run |
| Doc section + push | ~$0.05 | seconds (generated from the evidence JSON) |
Case-specific content: request-line name/suffix matching. Case-specific debugging: one round (suffix must exclude the HTTP/1.1 version token).
Scalability. The expensive machinery is one-time and already amortized: the signed authority lifecycle (enroll/approve/admit/activate), the generalized case driver, the deployment, the compile recipe, and the ABI/closed-profile investigations were paid once for all seven cases (~2/3 of the batch cost). Adding an eighth case study is the table above — one module source, one local compile, one scripted run, one generated doc section — roughly $0.5-0.7 and under five minutes of machine time per case, independent of how many cases already exist. That flat marginal cost is the scalability claim: portfolio growth is linear in cases, not compounding, and the per-case rule is bounded (97 lines of Rust keyed to one delivery shape).
Evidence appendix
Decision flow
The source checks the selected upload routes and suffix. This illustrates the matcher’s decision, not every Tomcat session-file placement path.
Complete normalized policy declaration
This is the full settings.remediation example included in the reader-facing case study. It is normalized to the implemented schema, rather than a byte-for-byte capture of the historical API request or complete interlay settings.
{
"version": 1,
"profile": "http-structure-v1",
"policy": {
"version": 1,
"rules": [
{
"scope": {
"methods": [
"PUT"
],
"path": "/uploads",
"descendants": true
},
"require": {
"kind": "deny"
}
}
]
}
}
Complete native module source
The complete available source for gabriel_tomcat_v1 is reproduced below from the reviewed repository snapshot. The source SHA-256 is 5123699e54ef8f70a2cab472875d92660ec6bad4b4ea511368695d217fbaa871. This identifies the source bytes, rather than independently proving the binary build or the recorded test results. The case’s experiment status and limitations still apply, including any failed import, partial coverage, or withdrawn claim. Original source comments are preserved verbatim.
//! Gabriel case module: session-store collision refusal for CVE-2025-24813.
//!
//! Tier-2 remediation for the Tomcat CVE-2025-24813 delivery shape: on the
//! upload routes, refuse only writes whose upload name collides with the
//! serialized-session store (`.session` suffix — the deserialization sink).
//! Ordinary uploads of any other name continue unchanged, preserving the
//! write capability (zero false positives). This no_std module owns no
//! storage, allocator, background work, or persistent state.
#![no_std]
use core::mem::size_of;
use core::panic::PanicInfo;
use eden_gateway_remediation::message::{Availability, MessageInput};
use eden_gateway_remediation::module::{
ApiHeader, CONTINUE, Decision, EDIT, EditSink, Edits, Failure, ModuleOutput, NEED_MORE, REFUSE, RemediationModule,
};
use eden_gateway_remediation::native_http_v3::{self as http, Api, Input, Request};
/// Upload routes in scope (the write-enabled default servlet surface).
const ROUTES: [&[u8]; 2] = [b"PUT /uploads/", b"POST /uploads/"];
/// The store-collision suffix this module refuses on those routes.
const SESSION_SUFFIX: &[u8] = b".session";
/// Stateless rule: refuse .session-named writes on upload routes.
struct RefuseSessionCollision;
fn request_line(head: &[u8]) -> &[u8] {
let end = head.iter().position(|&b| b == b'\n').unwrap_or(head.len());
let end = if end > 0 && head[end - 1] == b'\r' { end - 1 } else { end };
&head[..end]
}
impl RemediationModule<Request<'_>> for RefuseSessionCollision {
/// Wait for the complete head, then refuse only session-collision names.
fn evaluate(&self, request: &Request<'_>, edits: &mut Edits<'_>) -> Result<Decision, Failure> {
let _ = edits;
let _ = match request.header_count() {
Availability::Ready(count) => count,
Availability::NeedMore => return Ok(Decision::NeedMore),
Availability::Unavailable => return Err(Failure),
};
let message = request.message();
if !message.is_complete() {
return Ok(Decision::NeedMore);
}
let head = message.prefix();
let line = request_line(head);
let mut matched = false;
for route in ROUTES {
if line.starts_with(route) {
matched = true;
break;
}
}
if !matched {
return Ok(Decision::Continue);
}
// The upload name is the path segment after /uploads/, up to the space
// that starts the HTTP-version token (never part of the name).
let rest = &line[b"/uploads/".len()..];
let name_len = rest.iter().position(|&b| b == b' ').unwrap_or(rest.len());
if rest[..name_len].ends_with(SESSION_SUFFIX) {
return Ok(Decision::Refuse);
}
Ok(Decision::Continue)
}
}
static API: Api = Api {
header: ApiHeader { version: http::VERSION, table_size: size_of::<Api>() as u32 },
input_size: size_of::<Input>() as u32,
output_size: size_of::<ModuleOutput>() as u32,
sink_size: size_of::<EditSink>() as u32,
message_size: size_of::<MessageInput>() as u32,
state_bytes: 0,
capabilities: CONTINUE | REFUSE | NEED_MORE | EDIT,
required_inputs: 0,
evaluate: Some(evaluate),
};
unsafe extern "C" fn evaluate(input: *const Input, sink: *const EditSink, output: *mut ModuleOutput) -> i32 {
unsafe { http::invoke(&RefuseSessionCollision, input, sink, output) }
}
#[unsafe(no_mangle)]
pub extern "C" fn eden_http_remediation_v3() -> *const Api {
&API
}
#[panic_handler]
fn panic(_information: &PanicInfo<'_>) -> ! {
loop {
core::hint::spin_loop();
}
}
A file upload should create application data. It should not create a file that the application server later interprets as a serialized session. Our Tomcat experiment put Eden at that transition and compared two ways of preventing it.
The initial policy denied PUT requests under /uploads. The native module was more selective: a normal report.txt upload still returned 201, while the tested .session upload was refused with no deserialization marker at the backend.
These results came from a synthetic upload fixture modeling the placement prerequisite. The distinction matters because the real vulnerability involves Tomcat's path handling and a particular session-persistence configuration. This experiment establishes the behavior of the gateway intervention, without claiming that a filename suffix filter repairs Tomcat's complete path-equivalence problem.
Decision flow
The source checks the selected upload routes and suffix. This illustrates the matcher’s decision, not every Tomcat session-file placement path.
The vulnerability and the intervention point
CVE-2025-24813 depends on deployment conditions. Apache describes risks involving write-enabled default-servlet handling, partial PUT support and path equivalence; the RCE scenario additionally depends on file-based session persistence and a suitable deserialization library.
The defensible gateway objective is to stop the attacker-controlled file placement needed by a scoped attack path. It does not change Java deserialization or the server's filesystem interpretation after a request has already arrived.
Our lab made the effect observable with a fixture: writing an upload whose name ended in .session generated a simulated deserialization marker. Ordinary PUT uploads, a POST upload and a static GET supplied functional controls. This simplified model made it possible to compare policies without confusing a rejected request with a change inside Tomcat.
The first remediation and its functional cost
The structural policy denied PUT on /uploads and its descendants. That removed the tested partial-PUT path, but also denied ordinary PUT uploads.
It did not disable every upload mechanism. The original results explicitly retain an ordinary POST upload. The restriction should therefore be described as disabling scoped PUT uploads, rather than taking all uploads offline.
{
"version": 1,
"profile": "http-structure-v1",
"policy": {
"version": 1,
"rules": [
{
"scope": {
"methods": [
"PUT"
],
"path": "/uploads",
"descendants": true
},
"require": {
"kind": "deny"
}
}
]
}
}
The JSON above is a complete remediation declaration normalized from the recorded rule and checked against the pinned repository schema. Except where explicitly stated, it is not a recovered byte-for-byte management request. The original capture is needed to establish the exact settings and package bindings used in the run.
What the native module changed
gabriel_tomcat_v1 selected PUT and POST requests under /uploads/* and refused upload names with a .session suffix. Other upload names continued.
The implementation notes expose a small but consequential parsing issue. The suffix check initially needed correction to exclude the HTTP/1.1 token from the request line. A matcher operating on the whole line would not be evaluating the filename it purported to protect. The documented correction is part of the engineering story: the module has to select the intended protocol field before applying its predicate.
Even after that correction, a raw suffix rule has a limited claim. Canonicalization, percent decoding, separators, case handling, query strings and differences between a gateway path and a filesystem path can all affect what the origin writes. The fixture's .session convention is a test contract, not a derivation of every real Tomcat filename collision.
The observed before and after
The following results are reported by the pinned case-study record. They have not been independently rerun for this editorial rewrite.
| Request | Unprotected fixture | PUT-denial policy | Native module |
|---|---|---|---|
PUT of report.txt | 201 | 403 | 201 |
Static GET /index.html | 200 | 200 | 200 |
Tested partial PUT of pwn.session | 201; deserialization marker increases | 403; zero new effects | 403; zero new effects |
| Ordinary POST upload in the earlier policy run | 201 | 201 | Not separately reported in the module comparison |
The module preserved a write operation that the policy refused. That is the product result worth measuring: the application retained its ordinary PUT upload behavior while the tested session-file placement was blocked.
The backend marker adds the causal evidence. A refusal on the gateway side matters only if the origin did not receive and act on the write first. The record reports zero new deserialization effects after activation and restores the unprotected effect when the declarative policy is removed.
For a real deployment, the next decisive experiment is a vulnerable/fixed/protected comparison using the exact Tomcat configuration and the actual path-equivalence chain. Alternate writable routes, the manager application and direct origin access need explicit treatment. They are not covered by a rule scoped to /uploads.
Deploying the change and proving it was active
The policy and module arms used different deployment mechanisms. For the policy, an authorized administrator published settings.remediation through PATCH /api/v1/interlays/{interlay}. Readback showed state: ready with matching desired and effective digests. The API replaces the full settings object, so unrelated settings must be preserved. A successful submission alone does not establish that a listener is enforcing the intended revision.
The native module went through publisher-key enrollment, host inventory, inspection, independent host approval, a signed activation statement, admission and explicit activation. Admission returned phase: prepared and serving_now: false. The recorded activation then reported serving_now: true and acknowledgment from 2 of 2 expected shards. Those are local runtime shards, not evidence of two serving nodes or a protected fleet.
The statement was signed with Ed25519 over the SDK's canonical Borsh bytes, with the domain eden.module-admission.v1\0. Artifact, ABI, policy and scope bindings were part of the authority decision. Read-only inspection was refused with 403; a changed binding byte produced 400; a corrupted signature produced 403. The case used the native ABI v3 entry table eden_http_remediation_v3 and the closed native profile.
Revocation is especially important to interpret correctly. Removing the declarative policy restored the unprotected effect in the policy experiment. Revoking the native operation instead left mandatory protection in place and caused 503 E1A26, with nothing forwarded. A fresh approval and signed activation restored the module's behavior. Those are different controls with different availability consequences.
Complete tested native module source
The implementation tests raw request-line bytes. Its slice starts at an offset based on the length of /uploads/ from the beginning of the request line, not by parsing a normalized upload-name field; the tested suffix case still matched. The raw check does not prove equivalent behavior for decoded paths or query-bearing requests.
//! Gabriel case module: session-store collision refusal for CVE-2025-24813.
//!
//! Tier-2 remediation for the Tomcat CVE-2025-24813 delivery shape: on the
//! upload routes, refuse only writes whose upload name collides with the
//! serialized-session store (`.session` suffix — the deserialization sink).
//! Ordinary uploads of any other name continue unchanged, preserving the
//! write capability (zero false positives). This no_std module owns no
//! storage, allocator, background work, or persistent state.
#![no_std]
use core::mem::size_of;
use core::panic::PanicInfo;
use eden_gateway_remediation::message::{Availability, MessageInput};
use eden_gateway_remediation::module::{
ApiHeader, CONTINUE, Decision, EDIT, EditSink, Edits, Failure, ModuleOutput, NEED_MORE, REFUSE, RemediationModule,
};
use eden_gateway_remediation::native_http_v3::{self as http, Api, Input, Request};
/// Upload routes in scope (the write-enabled default servlet surface).
const ROUTES: [&[u8]; 2] = [b"PUT /uploads/", b"POST /uploads/"];
/// The store-collision suffix this module refuses on those routes.
const SESSION_SUFFIX: &[u8] = b".session";
/// Stateless rule: refuse .session-named writes on upload routes.
struct RefuseSessionCollision;
fn request_line(head: &[u8]) -> &[u8] {
let end = head.iter().position(|&b| b == b'\n').unwrap_or(head.len());
let end = if end > 0 && head[end - 1] == b'\r' { end - 1 } else { end };
&head[..end]
}
impl RemediationModule<Request<'_>> for RefuseSessionCollision {
/// Wait for the complete head, then refuse only session-collision names.
fn evaluate(&self, request: &Request<'_>, edits: &mut Edits<'_>) -> Result<Decision, Failure> {
let _ = edits;
let _ = match request.header_count() {
Availability::Ready(count) => count,
Availability::NeedMore => return Ok(Decision::NeedMore),
Availability::Unavailable => return Err(Failure),
};
let message = request.message();
if !message.is_complete() {
return Ok(Decision::NeedMore);
}
let head = message.prefix();
let line = request_line(head);
let mut matched = false;
for route in ROUTES {
if line.starts_with(route) {
matched = true;
break;
}
}
if !matched {
return Ok(Decision::Continue);
}
// The upload name is the path segment after /uploads/, up to the space
// that starts the HTTP-version token (never part of the name).
let rest = &line[b"/uploads/".len()..];
let name_len = rest.iter().position(|&b| b == b' ').unwrap_or(rest.len());
if rest[..name_len].ends_with(SESSION_SUFFIX) {
return Ok(Decision::Refuse);
}
Ok(Decision::Continue)
}
}
static API: Api = Api {
header: ApiHeader { version: http::VERSION, table_size: size_of::<Api>() as u32 },
input_size: size_of::<Input>() as u32,
output_size: size_of::<ModuleOutput>() as u32,
sink_size: size_of::<EditSink>() as u32,
message_size: size_of::<MessageInput>() as u32,
state_bytes: 0,
capabilities: CONTINUE | REFUSE | NEED_MORE | EDIT,
required_inputs: 0,
evaluate: Some(evaluate),
};
unsafe extern "C" fn evaluate(input: *const Input, sink: *const EditSink, output: *mut ModuleOutput) -> i32 {
unsafe { http::invoke(&RefuseSessionCollision, input, sink, output) }
}
#[unsafe(no_mangle)]
pub extern "C" fn eden_http_remediation_v3() -> *const Api {
&API
}
#[panic_handler]
fn panic(_information: &PanicInfo<'_>) -> ! {
loop {
core::hint::spin_loop();
}
}
The source is reproduced unchanged, including its original comments. Its signed admission, byte-level scope and recorded results are described above; comments in the code should be read in that context.
What this proof establishes
Eden demonstrated that a separately activated module could preserve the fixture's upload capability while removing a specific dangerous placement path. The durable repair remains a corrected Tomcat release. The immediate lesson is how to express and verify a narrow precondition at the gateway, and how carefully that precondition must be tied to the origin's real path semantics.