groundy
Security

Java and PHP Deserialization Defenses: Why One Layer Is Not Enough

Self-reported preprint data shows cross-layer Java and PHP deserialization defenses cut attack success to 6.2%, outperforming single-layer baselines that leaked up to 55%.

Published 6 references
A black thorned ceramic knot sits between two open forest-green toothed shells. One spike passes through a thin perforated ivory screen. Chipped copper-colored edges and hard shadows mark the warm ivory scene.
On this page12 sections

If you own a Java or PHP application with a deserialization RCE you cannot patch this quarter, the evidence now points to a concrete interim answer: harden the application and runtime/system layers together, and treat any gateway or WAF signature as a thin backstop rather than the control. In the self-reported evaluation behind the JANUS preprint, the baseline defenses (ModSecurity with the OWASP Core Rule Set, two RASP products, the Tetragon runtime enforcer, and Kintsugi, an uncoordinated application-plus-system combination) left 18.8-55.0% held-out attack success, while a synthesized cross-layer configuration cut that to 6.2% (self-reported, arXiv:2610.35532). The composition fully mitigated 13 of 16 tested deployments, but not all of them, and it costs latency: a 6.9% targeted P95 increase on the authors’ testbed, again self-reported (arXiv:2610.35532).

That ordering, application restrictions plus runtime policy first, edge rules second, is the finding worth acting on.

The interim window is the norm, not the exception

The reason this decision matters is that “just patch it” is often a multi-year plan, not a sprint. In a study of Java deserialization exploits and vulnerabilities, researchers observed that “the time used to remove gadgets varies between several months and almost 12 years, with an average of almost six years” (arXiv:2208.08173). Gadgets are the reusable code fragments inside libraries that attackers chain together after a deserialization entry point; removing them means refactoring library code that downstream applications depend on, which is why the lag stretches so long.

The same study found that for 24.1% of the examined CVEs (arXiv:2208.08173), the chosen resolution was a workaround rather than a patch modifying the vulnerable code. So if you are staring at an advisement that says “restrict deserialization” or “apply the vendor workaround,” you are in the statistical mainstream. Roughly one in four of these issues never got a code fix at all in that sample.

This reframes the question. Interim controls are not a stopgap you tolerate for a sprint; they are a control plane you may operate for years. That makes the choice of which layer to harden first, and what each layer actually stops, worth getting right.

What JANUS actually measured

The freshest evidence here is a single preprint, so it is worth being precise about its shape before leaning on its numbers. The authors state: “We evaluate Janus on sixteen vulnerability deployments across six Java and PHP applications against gateway, application, system, and cross-layer defenses” (arXiv:2610.35532). The system synthesizes a cross-layer defense configuration per deployment and evaluates it against held-out attacks, meaning payloads the synthesis process did not see, plus functionality tests to measure collateral damage on benign behavior.

The headline result, quoted from the preprint: “Janus reduces held-out attack success to 6.2%, compared with 18.8-55.0% for the baselines, fully mitigates 13 of 16 deployments, and preserves 98.4% of benign functionality tests at a 6.9% targeted P95 latency increase” (arXiv:2610.35532).

Every one of those figures is self-reported by the authors. There is no independent replication and no production deployment evidence in the preprint. That does not make the numbers useless; it makes them a bounded input. A threefold gap against the best baseline, and roughly ninefold against the weakest, is a strong signal even with a wide error bar. The exact decimals (6.2% and 6.9%, per the preprint) should be treated as lab figures that will move if anyone replicates the work.

The gateway illusion

If the first lever you can pull under time pressure is an edge rule, whether a WAF signature, a reverse-proxy filter, or a regex that matches the known exploit’s byte pattern, it is easy to point at in a ticket. It is also the layer with the weakest evidence behind it.

The JANUS baselines left 18.8-55.0% held-out attack success across the set (self-reported, arXiv:2610.35532). The set covers gateway rules (ModSecurity with OWASP CRS), application-layer runtime protection (two RASP products), system-layer enforcement (Tetragon), and Kintsugi, an uncoordinated application-plus-system combination that still leaked 20.3% (arXiv:2610.35532). Note what that last entry means: even a two-layer defense assembled without coordination sits inside the losing range, so the composition’s edge comes from coordination, not layer count alone. A defense that lets through somewhere between one in five and one in two unseen payloads is not a control; it is a filter that works against the payload you already know about.

Adjacent work explains the mechanism. WAFBooster benchmarks a shadow model and payload corrector against eight typical WAFs under SQL injection, cross-site scripting, and command injection attacks (arXiv:2501.14008): systematic payload mutation is exactly the stress signature-based blocking handles worst, and it transfers directly to serialized-object payloads. The paper’s own signature design states its assumption: “there exists a single payload substring, which remains unchanged in the process and unique enough to be used as a signature without causing false rejections” (arXiv:2501.14008), and its authors exclude DDoS on the grounds that “the payload is typically harmless and cannot be blocked by WAF.” The further step, that a serialized gadget chain whose bytes look like legitimate application traffic until the object graph assembles may present no boundary signature to match, is my inference from these pieces rather than a finding of either paper.

The scanner side tells a similar story. ShadowPickle, an attack on Python’s pickle ecosystem, “evades all scanners while keeping the Pickle file valid,” and its authors claim it is the first attack to bypass PyTorch’s Weights-only Unpickler (arXiv:2607.17503). The ShadowPickle authors report a 63% evasion rate across all scanners for their strongest variant, versus 42% and 41% for two comparison attacks (self-reported, arXiv:2607.17503). That is a different language ecosystem, so do not read it as a direct measurement of your Java gateway. Read it as what these two self-reported studies show: dedicated mutation effort defeated the pattern-matching and allowlisting defenses each paper tested, in two different ecosystems.

None of this says delete your WAF rules. It says a gateway signature answers the question “did we block yesterday’s PoC” rather than “will this hold next month,” and the budget you spend maintaining signatures is budget not spent on the layers that change the attacker’s problem.

Layer order for Java and PHP shops

The practical mapping onto tools you already have looks like this.

Application layer first. On the Java side, the filtering mechanism described in JEP 290 has existed since 2016 (arXiv:2208.08173), and deserialization filters “were introduced in Java 9 and back-ported” to earlier versions (arXiv:2204.09388). That back-porting matters: the application-layer control is deployable on legacy stacks without waiting for a JDK upgrade project. A serialization filter that constrains which classes may materialize, and how deep and large an object graph can get, attacks the deserialization entry point itself rather than the payload’s surface pattern. You are shrinking what deserialization is allowed to mean inside your process.

For PHP, the evidence in this evaluation is thinner, and it is worth being explicit about that. JANUS’s application controls were Java mechanisms, jdk.serialFilter settings for Java-native paths and the Jenkins class filter for XStream, and its benchmark’s single PHP application, Snipe-IT with two of the sixteen deployments, had exploit paths that supported only gateway and system controls (arXiv:2610.35532). Class-restriction research does exist for PHP, such as QUACK, which the paper’s related work describes as inferring allowed PHP classes by static duck typing (arXiv:2610.35532). But this evaluation does not exercise a PHP application-layer filter, so the application-first ordering here is Java-grounded; for a PHP deployment, what the benchmark actually tested is gateway plus runtime policy.

Runtime/system layer second. This is policy enforcement at the process level: what the application may execute, open, or connect to once code is running. The reason it pairs with application filtering is consequence containment. A filter that a mutated payload slips past still leaves the attacker needing the runtime to cooperate; a policy that blocks process spawning or outbound connections from the app server turns a filter miss into a failed exploit rather than a shell. The JANUS result is consistent with that mechanism: the cross-layer composition, which combines these layers, reached 6.2% held-out attack success where the baselines leaked 18.8-55.0% (self-reported, arXiv:2610.35532). One honesty note on the ordering: in JANUS’s ablations, disabling the system layer hurt most, raising attack success from 6.2% to 31.2% (self-reported, arXiv:2610.35532); disabling the application layer cost 14.1% and the gateway 20.3%. A strict ablation reading would rank the system layer first, and the application-first order above rests on deployability, the back-ported filter you can ship to legacy stacks, not on the ablation alone.

Gateway last, as a backstop. Keep the edge rules; they are cheap and they do block the lazy replay of public exploits. Just do not let their green dashboard stand in for the layers above.

LayerExample controlSelf-reported evidenceMain weakness
GatewayWAF/proxy signaturePart of baselines leaking 18.8-55.0% held-out attack success (arXiv:2610.35532)Payload mutation; only blocks recognized patterns (arXiv:2501.14008)
ApplicationJEP 290 serialization filter (Java)Available since Java 9, back-ported (arXiv:2204.09388); not exercised on JANUS’s PHP deployments (arXiv:2610.35532)Allowlist-style defenses can be evaded by dedicated attacks (arXiv:2607.17503)
Runtime/systemProcess-level policy enforcementPart of the winning cross-layer composition (arXiv:2610.35532)Tuning burden; regression risk per deployment
Cross-layer compositionSynthesized combination6.2% held-out attack success, 13/16 deployments fully mitigated (self-reported, arXiv:2610.35532)6.9% targeted P95 latency; 5/48 pairs still breached (self-reported)

The table’s last row is the argument: composition is the only configuration in the evaluation that moved the number materially, and it is also the only one with a real invoice.

The invoice ops forgets

Layering is not free, and the cost shows up in two places operations teams rarely budget for.

The first is latency. The cross-layer configuration preserved 98.4% of benign functionality tests at a 6.9% targeted P95 latency increase (self-reported, authors’ testbed, arXiv:2610.35532). Read that figure carefully: it is a targeted P95 on the paper’s testbed, meaning the tail latency on the operations the defenses actually touch, not a promise about your average request in production. Your number will depend on how deserialization-heavy your hot paths are. But the planning takeaway stands: if you layer application filtering and runtime policy, put a latency line item in the change request before you ship, somewhere around 7% on the affected paths as a first guess from this evidence (arXiv:2610.35532), and measure rather than assume.

The second cost is per-deployment regression, and here the JANUS comparison table carries a warning about vendor tooling. The paper reports that Kintsugi and Datadog RASP met the aggregate functionality reference but regressed on 4 and 8 deployments respectively, while its own configuration reported no regression on 15 of 16 deployments (self-reported, arXiv:2610.35532). The trap is the aggregate number. A tool that passes “98% of functionality overall” can still break your specific deployment, and 8-of-16 regression exposure means the odds of being that deployment are not small. Before you commit to any runtime-layer product, run it against your own functionality suite, not the vendor’s aggregate.

What still gets through

Honesty about the ceiling matters more than the headline. The same evaluation reports the winning configuration “is breached on 5 of 48 unseen-attacker/deployment pairs” (self-reported, arXiv:2610.35532). Under policy visibility, where the attacker can observe or infer the deployed policy, the breach count rose to 10 of 48 pairs, versus at least 23 of 48 for baselines; under consequence switching, the composition held at 9.7% attack success against at least 31.9% for baselines (all self-reported, arXiv:2610.35532). The authors trace the residual attacks to two synthesis failures and one held-out failure.

Two implications follow. First, layering lowers risk by a large factor but does not reach zero, and a determined attacker with visibility into your defenses roughly doubles the breach surface in this lab setup. Second, the residual failures were in the synthesis and generalization machinery, not evidence that layering is the wrong idea; they are evidence that any automated composition still needs a human checking the three deployments out of sixteen where it does not fully hold.

And the adaptive-adversary pressure is not hypothetical. ShadowPickle’s claimed bypass of PyTorch’s Weights-only Unpickler (arXiv:2607.17503) is a reminder that even purpose-built, allowlist-style deserialization defenses attract dedicated evasion work, and in that study’s self-reported results one such defense was bypassed. Plan for policy rotation and monitoring, not for a static configuration that holds forever.

The exit is still the patch

Interim layering buys time; it does not repay the underlying debt. On the long-term path, there is at least evidence that replacing native serialization is mechanically feasible rather than aspirational: ProDJ plain-code serialized production objects for all 44 projects in its study, 174,699 objects at the considered serialization points (arXiv:2405.11294). Moving serialization into explicit plain code aims to remove the implicit object-graph behavior that gadget chains depend on. That is a migration project, not a fix, but it is the kind of work the six-year average gadget-removal lag (arXiv:2208.08173) argues you should schedule rather than wait out.

The decision, with its limits

Given a deserialization RCE you cannot patch yet, I would harden in this order: application-level deserialization restriction first on Java (JEP 290-style filters, which ship back to legacy runtimes per arXiv:2204.09388), runtime/system policy second, gateway rules kept as a backstop. On PHP, the tested deployments offered only gateway and system controls on their exploit paths (arXiv:2610.35532), so runtime policy plus the edge is what this evidence supports there. Budget roughly 7% targeted P95 latency on the defended paths (arXiv:2610.35532), test functionality per deployment rather than trusting aggregate vendor claims, and keep the patch ticket open and funded, because 13-of-16 full mitigation is the ceiling this evidence offers, not the floor.

The limits deserve the same prominence as the verdict. Every JANUS figure in this piece is self-reported in one preprint with no independent replication and no production deployment evidence; the latency number is a targeted P95 on the authors’ testbed; 3 of 16 deployments were not fully mitigated and 5 of 48 attacker/deployment pairs breached the best configuration (self-reported, arXiv:2610.35532); and the benchmark’s PHP coverage is a single application whose exploit paths supported no application-layer control. The comparison also covers the authors’ chosen baselines, not your exact toolchain. Treat the layering order as a well-supported inference from one lab evaluation, and validate each layer against your own deployments before you call the interim done.

Frequently Asked Questions

How long does it typically take to remove deserialization gadgets from Java libraries?

In a study of Java deserialization exploits and vulnerabilities, researchers observed that “the time used to remove gadgets varies between several months and almost 12 years, with an average of almost six years” (arXiv:2208.08173).

What latency impact is associated with the cross-layer defense configuration?

The cross-layer configuration preserved 98.4% of benign functionality tests at a 6.9% targeted P95 latency increase (self-reported, authors’ testbed, arXiv:2610.35532).

Does the cross-layer defense fully mitigate all tested deployments?

The same evaluation reports the winning configuration “is breached on 5 of 48 unseen-attacker/deployment pairs” (self-reported, arXiv:2610.35532).

References

Follow the links in the article for context. The supporting material is collected here for further reading.

  1. JANUS preprintarxiv.orgAccessed
  2. WAFBooster benchmarkarxiv.orgAccessed
  3. ShadowPickle attackarxiv.orgAccessed
  4. Java serialization filter backportarxiv.orgAccessed
  5. ProDJ plain-code serializationarxiv.orgAccessed

Join the discussion

Share a useful perspective or ask a question about this article.

Discussion guidelinesComments privacy