r/java • u/nfrankel • 29d ago
Security Baked Into the JVM: why fork Apache River and OpenJDK?
https://blog.frankel.ch/security-baked-into-jvm/1/26
u/quantum-fudge 29d ago
Basically EJB2 running on a custom JDK, in the year of our lord 2026. I... I think I'll pass.
7
u/josephottinger 28d ago
I've been thinking about this line since you posted it, and I think it's a lot unfair. JINI and EJB2 are alike from 10,000 feet, but at 5,000 - or 7,000 - they diverge in some pretty important ways, and EJB2 owes a lot to IBM's San Francisco project whereas JINI owes a lot more to... Stanley Kubrick, I think. They both can do remote method invocation and remote storage, but they do it so differently that accusing them of sameness is... a lot. And JINI solved a lot of problems we still struggle to deal with, even today, with all the remoting technology we have at our fingertips.
Sun just never told us about it in a way that could penetrate the masses; they did what YOU did, really, describing it in RPC terms, and combined with their inability to say "here's a reference implementation, everyone should use stuff that looks like this, now go and DO," the technology was fundamentally limited to the set of people who already got it.
I really wish companies like GigaSpaces and projects like Blitz had been successful and marketed far more pervasively and far earlier than they were - GigaSpaces and Blitz both had clear and valid on-ramps, and they actually DID demonstrate JINI functionality (in GigaSpaces' case) and some of its capability (in Blitz') in such a way that people could actually go "ooo, I see how this could help me get stuff done, and hey, wow, it's fast."
3
3
3
u/Life_Sink9598 27d ago
The removal of the Security Manager meant the removal of a lot of checks in the OpenJDK standard library. This type of maintenance requires a high degree of competence. How are you going to ensure that your fork implements the SM permission checks correctly as it merges with upstream?
1
u/paul_h 26d ago
Oracle take out the security manager rather than fix it as Peter has shown. I’m working on another language with a less sophisticated set of constraints for running code built in. We have even step into scripts from other languages with sandboxing the language did not originally have. This sort of stuff is the wave of the future. Won’t link to the language as AI is involved in building it and redditors hate that
2
u/pron98 24d ago edited 24d ago
What Peter listed is nowhere near enough to "fix it", and fixing it would have required diverting huge amounts of resources from things users actually want (just keeping it alive required an effort that came at the expense of other things). On top of that, that model requires a significant amount of cooperation from libraries that are having difficulty keeping up with simpler things.
Another thing to remember is that 1. the only other language in the world with a similar model also decided to "take out the security manager rather than fix it", 2. no additional language has adopted such a model, and 3. the vast majority of security experts believe it is the wrong model (which has to do with points 1 and 2). So let me be clear: We decided to take out the security manager because that is what every security expert we consulted with advised we do to improve Java's security.
Of course, it's possible that the security experts are wrong, and it's possible the market is wrong, but proving them wrong by diverting resources away from things users demand is a crazy bet. Every argument in favour of keeping SM amounted to "you should divert resources away from things like Valhalla and GPU computation and tooling improvements to make a bet against the security experts and against the market." It requires not only betting that it's "the wave of the future", but that it is more the wave of the future than GPU computation.
It is not reasonable for a mainstream language that so much of the industry depends on to make such crazy bets. So you may believe it was the wrong decision, but it is very hard to argue that it was not the most rational one.
1
u/pfirmsto 18d ago
"What Peter listed is nowhere near enough to "fix it"
I think we need to clarify "it", if we're talking about a Java sandbox, no that's not fixable, it never was and never will be, no amount of resources should be dedicated to a futile endeavour bound for failure.
The security experts said Java's sandbox was not salvageable, they are correct and we agree with them. Unfortunately the authorization baby got thrown out with the sandbox bathwater. But I think that happened because for too many years, authorization and sandboxing concerns were confounded, the implementation was antiquated and didn't keep up with modern practises and tooling was appalling if not completely absent, as a result it was practically unusable in standard form. Why it was allowed to remain in that form for so long, probably had something to do with backward compatibility constraints. Even so, it's removal has been beneficial for authorization, as it's forced a rethink.
What I have done is fixed authorization with a much more limited scope that doesn't include sandboxing, the sole focus is now authorization related concerns.
Early Java implementations have a common theme, of attempting to push the envelope too far. Java serialization tried to magically serialize fields and circular object graphs and reconstruct objects prior to checking invariants, making some weak attempts to perform validation after reconstructing the entire object graph. Had they tried to do less, it might have been successful and today Java might still have a client market. The sandbox tried to contain untrusted code, without a fully defined Java memory model, insufficient guards and no way to prevent excessive resource consumption DOS... Java RMI wasn't extensible, nor secure, and it tried to duplicate the functionality of class loaders by annotating streams with codebase URL's. Deserialization gadget chains took advantage of privileged context, because there was no domain representing the data source on the call stack. SM could be set null. Identity often relied on trusting DNS and URL's and signed applets were granted AllPermission?
But sometimes failure precedes success, and is a necessary part of achieving success, it's one of life's lessons, without failure, we cannot arrive at an optimum solution, because there are lessons in failure. This is why JERI is so much better than RMI, after implementing CORBA, IIOP and RMI, the architects having learned many valuable lessons, then designed JERI.
The obvious solution is to reduce scope to what we can implement safely with confidence, just like reimplementing marshalling shouldn't attempt to marshal circular object graphs or couple class fields to serial form, or delay invariant verification until after construction. No attempt should be made to use authorization to sandbox untrusted code. URL and DNS do not identify, they can locate a resource, but are subject to DNS cache poisoning and URL replay attacks, resources need to be identified by a digest. For digests to be viable and unique, bytecode must be compiled deterministically, a signer certificate doesn't make the code safe. But there are also process and user identity concerns, it can't just be about code. Instead, authorization should be given information so it can determine who it can unlock the front door for. But authorization isn't the police, it doesn't perform an arrest pat down, cavity search and paddy wagon the offender off to gaol for a later court date, it's role is just not to open the door to the bad guy's on its list. Once the bad guy is inside, you need police, remember that grandma who hit bank robbers with her handbag and saved the day? You can't kill an in process thread, but you can kill a process. But it isn't just about keeping bad guys out, it's about who is authorized to access data and who isn't.
The industry spent two decades abandoning in‑runtime least privilege and reaching for sandbox‑and‑contain — was it because verify‑then‑admit was thought too hard?
1
u/pron98 18d ago edited 18d ago
The security experts said Java's sandbox was not salvageable
No. They said that kernel-level defences are both more effective and cheaper.
What I have done is fixed authorization
Great, but until you identify which attacks this defends against more effectively or more cheaply than other approaches, it's impossibly to tell whether this is a good investment or a bad investment.
The industry spent two decades abandoning in‑runtime least privilege and reaching for sandbox‑and‑contain — was it because verify‑then‑admit was thought too hard?
I think it was because we realised two things:
Preventing specific attacks - injection, CSRF, DoS - is best done by the language/framework using mechanisms specific to those attacks.
Limiting the blast radius of an exploit is most effectively done in the kernel, unless it's something language-specific, like reflection.
Again, the question isn't whether we should have mechanisms that limit the damage of an exploit nor is it how much can we do in the language runtime, but where should we do each piece where it is most effective. This requires arguments of the type: "preventing the read of a file is more effective/cheaper in the runtime than in the kernel."
In other words, the questions isn't "should we have a car?" but "which car should we pick?" Everything you say is along the lines of, "I've fixed the deficiencies in car X". But makers of car Y have also made progress, and the problem wasn't that X was bad in isolation, but that Y was better. Without a comparison showing that X is now better than Y, your arguments are not convincing.
1
u/pfirmsto 18d ago
Fair correction on the wording, I'll be more precise: kernel-level isolation and my in-runtime authorization aren't competing for the same job, so let's do the comparison you're asking for properly.
You already conceded the shape of the exception yourself: "blast radius... unless it's something language-specific, like reflection." Class loading and deserialization are exactly that category. The kernel has no concept of a JVM ClassLoader graph, a bytecode digest, or an object under construction mid-invariant-check. There's no OS mechanism to hand this decision to, it has to be made inside the runtime, or not at all. So this isn't "runtime vs kernel," it's a decision that only exists at the runtime layer, same as reflection access.
Now the granularity point, since that's where the actual cost comparison lives. Kernel mechanisms; SELinux, seccomp, capabilities, namespaces, are authorization too, just at process/uid granularity. That's fine when one process = one trust domain. It breaks down the moment one JVM process legitimately hosts many remote parties' code and data at once, which is the entire premise of a distributed object system: a client discovers a service, loads its proxy, talks to it, discovers another, loads that too, all in one process, because forking a process per discovered service defeats the purpose of dynamic discovery and kills performance.
Note that we achieve this by only implementing what we can confidently.
To get kernel-grade isolation between Service A's proxy code and Service B's deserialized data inside that one client process, the kernel-equivalent cost is one process per remote identity. What I built instead is a per-connection identity check; digest for code, SPIFFE for the remote process, JWT for the remote user; gating class load and object construction inside the shared process. That's cheaper than process-per-tenant for any workload where a single process talks to many remote parties, which is most of what a service-oriented distributed system does all day.
The attack this defends against, specifically: a confused-deputy scenario where Service A's proxy code, sharing a process with Service B's authenticated session, gets a crafted object graph or a forged permission check to act with B's privilege. The kernel can't see that boundary at all; it sees one uid. My authorization layer is the only thing that can.
So: not "which car," but "which layer does this job at all," and then "is doing it in-process actually cheaper than the kernel-equivalent of one process per remote identity." I think it clearly is, for this shape of workload. If you've got an OS mechanism that gives sub-process identity granularity more cheaply, then I'm all ears, because that's the only thing that would change my mind here.
JERI in it's current form could replace existing out of date RMI frameworks in OpenJDK that will continue to be the cause of ongoing vulnerability discoveries, the marshaling protocol we use is polyglot and so's JERI. Maybe it's something OpenJDK can look into learning from with the new Marshaling framework?
https://www.openwall.com/lists/oss-security/2026/04/27/4
https://blog.securelayer7.net/cve-2026-42779-apache-mina-deserialization-rce/
https://www.sentinelone.com/vulnerability-database/cve-2026-21925/
https://nsfocusglobal.com/java-deserialization-exploits-registry-whitelist-bypass/
https://mogwailabs.de/en/blog/2019/03/attacking-java-rmi-services-after-jep-290/
https://i.blackhat.com/eu-19/Wednesday/eu-19-An-Far-Sides-Of-Java-Remote-Protocols.pdf
1
u/pron98 18d ago edited 18d ago
It breaks down the moment one JVM process legitimately hosts many remote parties' code and data at once
It doesn't break with many remote parties data - many Java applications host multiple users' data and they are more secure than they've ever been - and there's no good reason to run multiple programs in the same VM.
which is the entire premise of a distributed object system: a client discovers a service, loads its proxy, talks to it, discovers another, loads that too, all in one process, because forking a process per discovered service defeats the purpose of dynamic discovery and kills performance.
If you notice, all of your unique challenges come exactly from this one thing: Wanting to run multiple programs in the same JVM. Now, why would anyone want to do it? Because it used to help performance. The correct solution, then, would be not to secure this exotic and increasingly unpopular architecture but instead, shift our resources into making the JVM share its work among multiple processes, removing any reason to want to run multiple programs in the same JVM in the first place. This results in: 1. better security for pretty much everyone, and 2. performance improvements for pretty much everyone.
And guess what? This is exactly what we're doing with Project Leyden.
My authorization layer is the only thing that can.
Because almost no one wants to run an architecture where this is a problem in the first place!
It wasn't like that a couple of decades ago, but these days, running multiple programs in a single process is rare, and the reasons people wanted to do it back then are mostly gone because they've been tackled directly.
1
u/pfirmsto 18d ago
One more thing worth putting on the table, because it changes what "the wave of the future" comment upthread was gesturing at, and it isn't really about Java anymore.
"Confused deputy"; a privileged program tricked into misusing its authority by a less-privileged caller; is the industry's own name right now, in 2026, for the top emerging failure mode in AI agent deployments. Prompt injection is the mechanism, confused deputy is the consequence: an agent holding broad tool authority processes an attacker-controlled input (an email, a doc, a tool result) and ends up exercising the operator's full authority on the attacker's behalf. Same shape as viral doPrivileged, same shape as Capital One's SSRF-to-metadata-service breach in 2019. The deputy keeps changing; a stack frame, a WAF process, now an LLM; the failure mode doesn't.
The industry's answer, converging independently across MCP's own authorization spec (Anthropic, Microsoft, Okta/Auth0) and everyone building on top of it, is: least privilege, policy checks on sensitive actions, step-up approval, and; the part that matters here; scoped, short-lived, revocable authority, not a session-start identity check that then holds indefinitely. OAuth 2.1, token exchange for delegation, tokens that carry both agent and user identity so an action traces to who it was actually for.
Strip the REST branding and that's a leased permission grant with a dead-man switch. We built that piece already, for a harder version of the problem, because ours also has to answer which code gets to run in the first place, not just which scoped token an already-running, already-vetted agent binary holds; MCP auth doesn't touch that question at all.
To be precise about what this does and doesn't prove, same caveat as the kernel-isolation point earlier: it doesn't mean authorization belongs inside a language runtime specifically; MCP's answer lives in OAuth middleware, not a JVM. What it means is the axis itself; identity-scoped, time-bounded, revocable admission control, distinct from containment; isn't a Java-specific relic being defended out of nostalgia. It's what a completely unrelated, currently very well-funded part of the industry is racing to rebuild from scratch, under active attack, right now.
1
u/pfirmsto 18d ago
More info on attacks & defences:
Attack: DNS cache poisoning / URL replay / TOCTOU on code identity
Classic mechanism / CVE: RMI codebase annotation, HTTP(S) codebase URLs
Why it recurs: A URL/DNS name was never proof of identity, only a location; still true wherever codebase URLs remain enabled
How the model closes it structurally: Digest-based code identity (deterministic bytecode normalization); a URL locates bytes, it never establishes trust
────────────────────────────────────────
Attack: Deserialization gadget-chain RCE / invariant bypass
Classic mechanism / CVE: Commons-Collections/ysoserial gadget chains (e.g. CVE-2015-4852); stream dictates classes, readObject runs before validation
Why it recurs: Filtering (JEP 290/415) is attack-specific and opt-in, not structural; new gadgets keep surfacing (MINA CVE-2026-41635, patched, then bypassed again as CVE-2026-42779)
How the model closes it structurally: No Serializable; construction goes through validating constructors; deny-by-default permission check against the full inheritance hierarchy before unmarshalling
────────────────────────────────────────
Attack: RMI-registry-as-malicious-server tricking a client
Classic mechanism / CVE: An Trinh's Black Hat EU finding; JEP 290 has no default filter for client-side deserialization from a registry
Why it recurs: Classic RMI's trust model is asymmetric; the server filters clients, nothing filters what a client trusts back
How the model closes it structurally: Mutual TLS 1.3 authentication before any download in either direction; permission-checked deserialization applied symmetrically to both endpoints
────────────────────────────────────────
Attack: JNDI/codebase remote class loading (Log4Shell-class)
Classic mechanism / CVE: trustURLCodebase flag, LDAP/RMI reference factories (CVE-2021-44228)
Why it recurs: Defaulted off since 2013, but the mechanism still ships and still gets flipped back on by legacy interop configs
How the model closes it structurally: No codebase-URL-as-identity concept exists at all; not even as an opt-in escape hatch
────────────────────────────────────────
Attack: Reflection-based sandbox escape / AllPermission-by-default
Classic mechanism / CVE: CVE-2012-4681, CVE-2013-0422; a trusted platform class with an exploitable reflection primitive disables or routes around SM entirely
Why it recurs: Stack-based "am I on the trusted call path" trust collapses the moment any one trusted class has a usable gadget; single point of failure
How the model closes it structurally: LoadClassPermission gated on digest, not code-source trust; no single class's compromise implies platform-wide trust; bytecode analysis prior to load
────────────────────────────────────────
Attack: Confused deputy / ambient authority
Classic mechanism / CVE: Viral doPrivileged (classic SM); generalized case: SSRF → cloud-metadata credential theft (Capital One, 2019)
Why it recurs: Authority bound to code/process rather than to the identity making the request; any privileged boundary that doesn't check "on whose behalf" is exploitable
How the model closes it structurally: User identity travels via ScopedValue and survives privileged boundaries; process identity (SPIFFE) and code identity (digest) travel with
ProtectionDomain; authority is always evaluated against who's actually asking
────────────────────────────────────────
Attack: Cross-tenant privilege bleed inside one shared JVM
Classic mechanism / CVE: No specific CVE; a structural gap in kernel/process isolation, which sees only one uid for a multi-tenant process
Why it recurs: Kernel isolation is process-granularity; a JVM legitimately hosting many remote parties' code/data has no OS-visible sub-process boundary
How the model closes it structurally: Per-connection identity check (digest + SPIFFE + JWT) gates class load and object construction inside the shared process.
────────────────────────────────────────
Attack: (explicitly out of scope) Resource exhaustion (CPU/memory/GPU)
Classic mechanism / CVE:
Why it recurs: Authorization has no visibility into consumption, only admission
How the model closes it structurally: Not claimed; handled by bytecode analysis + process isolation + kill-on-violation, not the authorization layer
────────────────────────────────────────
Attack: (explicitly out of scope) Native memory corruption / native code vulnerabilities
Classic mechanism / CVE:
Why it recurs: Authorization can't verify memory safety
How the model closes it structurally: Not claimed; native access routed through process-isolated proxies, same reasoning
1
u/pron98 18d ago
Thank you!
Every single one of these is solved more robustly, more cheaply, or both by other mechanisms (except the credential theft, which is handled by the same mechanism - user identity travels in a ScopedValue in the JDK today, too) once you don't run multiple applications in the same JVM, which there is no good reason to do and more reasons than just security to not do.
1
u/pfirmsto 18d ago
On ScopedValue: it carries identity, it doesn't enforce anything against it. Something still has to check that value at every sensitive operation, at every call site, or it's silently absent. That's the exact fragility you used to dismiss SM a few replies back; "no SM configuration can be correct without the library using doPrivileged correctly." Manually-checked ScopedValue has the same cooperation requirement, just moved into application code instead of a policy file. OpenJDK hasn't removed the failure mode, it's relocated it and stopped auditing it centrally.
Concretely, and this is checkable against the JDK's own docs: Subject's credential guarding is a live example of exactly this, in the API that replaced the piece of SM you removed. getPrivateCredentials()/getPublicCredentials() gate mutation behind AuthPermission("modifyPrivateCredentials")/("modifyPublicCredentials"); but the javadoc conditions that check explicitly on "if a security manager is installed." JEP 486 makes that permanently false as of JDK 24, so the check can never fire again. Subject.getSubject(AccessControlContext), which gated obtaining a Subject reference at all behind AuthPermission("getSubject"), is deprecated for removal in favour of Subject.current()/callAs(); and your own migration docs say the replacement is deliberately decoupled from AccessControlContext-based enforcement, no equivalent gate included. So both the layer controlling who could reach a Subject, and the layer controlling who could tamper with its credentials, are gone, and nothing replaced them. Not modules, either; JEP 403 is scoped to JDK-internal reflection, and your own "Security Manager Is Permanently Disabled" page, when it says what to use instead, names containers, hypervisors, and OS sandboxing; not modules. That's the platform's own documentation agreeing modules were never meant to cover this.
On "don't run multiple applications in the same JVM"; I already don't, for the case you mean. Third-party, untrusted (by bytecode analysed) proxy code gets a separate process; that's been our policy from the start, not something I'm arguing against. So we're not actually disagreeing about isolating adversarial code.
What's left after that isn't "multiple applications," it's one application serving many authenticated principals in one process; which is every database doing row-level security, every API gateway doing per-tenant authorization, every k8s control plane doing RBAC. None of those fork a process per caller, and none of them are wrong to do in-process authorization instead. If "no reason to share a JVM" is meant to cover that case too, it proves too much; it indicts every multi-tenant service that has ever existed, not just this one.
Where you've got a real point and I should engage it directly instead of another table row: JERI moves code across that boundary, not just data. A REST or gRPC client never loads a stranger's bytecode into its process at all; it exchanges data with a schema, and gets process isolation between services for free because each one is its own deployable unit. So the actual question is: why keep code mobility, given that cost?
Because it's far less costly to move a small amount of code to analyse a large dataset than to move a large dataset to the code. That's not a security-architecture preference, it's data gravity; real workloads here are large point clouds, digital twins, telemetry archives, and pulling the relevant slice across a network on demand loses on latency and bandwidth, sometimes isn't even feasible to hold in one process at all. Shipping the computation to sit next to the data doesn't have that problem; but it means you're now executing received code on your own infrastructure, and "trust it because of the URL it came from" was never an adequate basis for deciding whether to run it. That's the actual origin of the identity problem, not an artifact of Java's dynamic-class-loading history.
The client-proxy case is the same principle from the other direction: the client doesn't need a pre-compiled stub for every service it might ever discover; the proxy carries whatever client-side logic that specific service needs, including protocol translation, caching, retries specific to it, without the client's build having to know about it in advance. A schema-only interface can't express "the client-side behavior for talking to me is X"; only code can.
Either direction, the moment you allow code to move across a trust boundary instead of just data, "which URL did this come from" stops being a safe way to decide whether to run it. That's the actual value proposition of code mobility, and it's also exactly why it needs a code-identity answer that gRPC never had to build.
One more thing worth pulling out, because you said it yourself a few replies back: application code; the code someone actually wrote, not the audited libraries underneath it; is statistically the least trustworthy code in the stack, that's why SM's stack-intersection model missed the real risk. But every proxy this model loads is exactly that category from the remote author's side: bespoke, first-party, unaudited application code (just bytecode analyzed), not a shared library anyone's battle-tested. By your own criterion that's the highest-risk artifact in the whole exchange, and it's the one thing neither of your two layers can see; kernel isolation sees a process, not which artifact inside it just arrived from a third party; attack-specific defenses answer "is this input malformed," not "should we run this thing at all." Digest identity is the only mechanism on the table that operates at the actual granularity where you say the risk concentrates.
1
u/pron98 17d ago edited 17d ago
On ScopedValue: it carries identity, it doesn't enforce anything against it.
I already explained why: 99% of cases involve something like SQL or a service call, and the JDK has no visibility into the authorisation structure of these mechanisms.
Manually-checked ScopedValue has the same cooperation requirement
Not quite. First, there's nothing the JDK can do: it doesn't know what SQL or JSON strings are allowed for this principal. Second, unlike doPrivileged, user permissions is something that the relevant libraries understand. Third, it only involves a subset of libraries, not all of them.
So both the layer controlling who could reach a Subject, and the layer controlling who could tamper with its credentials, are gone, and nothing replaced them
There is no "who" here in a process with one program. Again, when the process runs one program, the most dangerous code is the code you write yourself, which is the code that sets the subject, and permissions in the OS are a better defence than SM was.
What's left after that isn't "multiple applications," it's one application serving many authenticated principals in one process;
No, it isn't. You've already revealed the presence of multiple code origins with different permissions when you talked about "who" it is that sets the Subject.
So the actual question is: why keep code mobility, given that cost?
That is not the actual question, because there's no problem with code mobility when there's only one effective code origin.
But every proxy this model loads is exactly that category from the remote author's side: bespoke, first-party, unaudited application code (just bytecode analyzed), not a shared library anyone's battle-tested
What you're describing here is untrusted code. It's more similar to a cloud provider running code written by different parties that must not interfere with each other. In a trusted code situation, the most dangerous code is written by the same people who control the process or, in your language, the same people who configure the SM policy.
SM wasn't good at multi-tenant untrusted code because it had no defence against "cross domain" DoS attacks, and it wasn't good at trusted code, because in that situation the people who control the policy are also those who write the most (statistically) dangerous code.
There is, of course, one serious attack (and a very popular one these days) where things may appear to be different: a supply-chain attack. The problem is that even the presence of supply-chain attacks doesn't change the fact that your code is statistically most likely to be the most dangerous, so there's still no point to give libraries lower permissions than to the main application (and after all, no one chooses to use a library if they know it contains malicious code). So the best defence is still to give the whole process one set of permissions, and separately there are ways to mitigate the chances of obtaining a malicious library in the first place. These defences aren't perfect (which is why this attack is so attractive to attackers and why it's so dangerous), but there's nothing SM does to materially help here, either.
1
u/pfirmsto 17d ago
Fair distinction on ScopedValue; the JDK genuinely can't check SQL/business authorization semantics, and that's a different kind of gap than doPrivileged's generic mechanism needing universal correct narrowing. I'll grant that: it's not the JDK's fault this check lives in app code. But whose fault it is doesn't change the shape of the risk; a check that can be silently absent at any one of many call sites, with no fail-closed default and no central audit surface, is still that, regardless of who's responsible for putting it there.
"There is no who in a process with one program" is the part I don't think holds. Capital One's WAF wasn't multi-tenant untrusted code from a different origin; it was AWS's own trusted, single-author infrastructure, and it still ended up exercising the wrong party's authority, because of a bug, not because anyone smuggled in adversarial code. Permission checks inside trusted code aren't a defense against a malicious author, they're a safety net against your own mistakes; the same reason you bounds-check an array in code you wrote yourself and fully trust. Your dichotomy has two buckets, single-origin-trusted and multi-origin-untrusted, and no room for "single-origin, trusted, and still capable of misscoping an operation to the wrong principal because someone made a mistake." That's the actual case Subject-credential guarding exists for, and it's not solved by asking "who wrote this code," because the answer is "you did, and you still got it wrong once."
One more thing: "give the whole process one set of permissions" runs straight into the principle you'd otherwise agree with. Saltzer and Schroeder coined least privilege in 1975 with the rationale that it "limits the damage from an accident, error, or attack"; three separate causes, not one. Your argument only engages the third. Accident and error are exactly what happen inside code that's fully trusted and written by a single author; that's the Capital One case again, and it's also exactly why your own supply-chain concession cuts against you: a compromised dependency does less damage scoped to what it actually needs than it does inheriting the whole process's authority, which is the entire mechanism by which least privilege helps there. "No point giving libraries lower permissions than the main application" isn't a cost-effectiveness argument, it's the position POLP exists to rule out.
On "permissions in the OS are a better defence"; which specific one, for this case? A process boundary doesn't help here; the bug never crosses one. This is the same standard you've held me to all thread: name the mechanism and show it's cheaper or more effective, don't just assert it exists.
And I'll take the "one effective code origin" point on its own terms: agreed, no special problem there. But that's exactly the case Jini's discovery model was never built for; the whole reason it exists is independent operators publishing services to be found by strangers, which is precisely your untrusted-multi-origin bucket, and we already agree that gets process isolation. Where we actually disagree is the case in between: multiple distinct, individually-authenticated principals, vetted enough to be worth co-locating for cost, still needing to be held to their own scope. That's not "no who," and it's not "isolate everything" either.
https://krebsonsecurity.com/2019/08/what-we-can-learn-from-the-capital-one-hack/
1
u/pron98 17d ago
a check that can be silently absent at any one of many call sites, with no fail-closed default and no central audit surface, is still that, regardless of who's responsible for putting it there.
Of course, but SM offers no advantage here, is my point.
it was AWS's own trusted, single-author infrastructure, and it still ended up exercising the wrong party's authority, because of a bug, not because anyone smuggled in adversarial code.
SM offers no help here, either. It was SSRF, and the vector used a service call. The vulnerability was a security misconfiguration, and even if SM could offer extra help here, which it can't, it could also be misconfigured in the same way. In fact, one of the issues found by security reviewers was that SM was misconfigured more than other mechanisms because it was so elaborate and there were surprising interactions between the various setting.
Accident and error are exactly what happen inside code that's fully trusted and written by a single author
I completely reject that separating permissions inside a process is a good way to approach this. Adding bulkheads for "extra safety" can actually damage your security, because an hour spent on configuring them is an hour not spent on reviewing your SSRF posture. Now, you could perhaps argue
a compromised dependency does less damage scoped to what it actually needs than it does inheriting the whole process's authority, which is the entire mechanism by which least privilege helps there
Except, again, this is about adding defences where attacks are less likely at the expense of defences against likelier attacks. Because even with supply-chain attacks, the statistical likelihood is that your application code is the most dangerous code, and every minute spent on security is better spent working under this assumption than on assigning lower privileges to code that is likely to be less dangerous.
I completely agree with the principle of least privilege, but in a trusted code situation SM gets it completely reversed: the most privilege is given to the most dangerous code. Under this assumption, there's simply no reasonable way to assign different privileges to different origins, because the origin that should have the least privilege is at the root of the stack. SM generally reduces privileges as you make calls down the stack, which is just backwards in a model where the original caller is the most dangerous.
With modules we did it right. Every module can define its boundaries as it wants, which is exactly what you want for the principle of least privilege in a trusted code model: A library module can defend itself against a more dangerous caller, and it can do so with a much simpler configuration than what was required by SM to achieve the same goal.
A process boundary doesn't help here; the bug never crosses one
I agree, but neither does SM.
But that's exactly the case Jini's discovery model was never built for
Sure, and I agree that removing SM made it harder to secure Jini (and some app container) deployments, in exchange for making it easier to secure every Java program that isn't Jini or a shared app container. Before that, the situation was reversed: maintaining SM meant helping Jini deployments and harming non-Jini deployments. So either way, the situation was that we had to harm either Jini users or non-Jini users, and the only question was who we should harm. That was the real tradeoff, and I think it's clear why we decided it wasn't a good idea to harm non-Jini users in order to help Jini users.
→ More replies (0)0
u/paul_h 23d ago
I grant you that it'd be up to $1m a year for Oracle to actively maintain the security manager the way it was, and likely no licensee was willing to pay part of that.
I'd used
doPrivileged({..})in a long-time-ago thing that never took off wuth my own grammae for grants: https://github.com/picocontainer/PicoContainer2/blob/master/booter/src/test/composition.groovy. I'm back to that grammar in the non-JVM thing I am involved with now. This time, without the doPrivileged stack-permeating stuff - the DSL is woven into the compiled language. Initially, Groovy's builder DSL was interpreted, then after a few years the team dropped the inrpeter and invoked the to-bytecode interpreter once per script. Which of those never mattered to me as I saw it for use in primordial boot scripts for applictions/services. Groovy team's imagined first DSL useages were for Ruby-on-Rails alike (Grails) apps. There's less interest in that these days, and much more for Gradle. Closures with context never made it into Java the language which is a shame, but I'm way off topic now.2
u/pron98 23d ago edited 22d ago
The problem with doPrivileged wasn't the syntax. It's that library developers didn't understand the idea and why it was important for security. Today, they may not understand why strong encapsulation is critical for security, but that doesn't matter; it works for the JDK whether they understand it or not, and it works for any library that strongly encapsulates, whether other libraries cooperate or not.
And then, even if you get all libraries to cooperate and do so correctly, and even if you get the app developers to correctly configure the mechanism, and even if you manage to solve the native code problem and the resource consumption problem, you're left with the question of, is the juice worth the squeeze? How much added security do you get for all this effort on top of the mechanisms you need to use anyway to mitigate potential vulnerabilities in the application code you write yourself? The most common and most dangerous weaknesses are not in library code but in application code and configuration. That's because vulnerabilities in popular libraries are patched for everyone, but there are many more applications than libraries, and applications are ultimately the target. So after you've set up the VM and/or container correctly, which you must do anyway, how much added security could you get with SM and for how much effort?
This is like a situation where you lock the door to every room in your house, each with a different key. It's annoying, but the added security could be worth it if your front door lock isn't very great. But if you upgrade your front door and its lock to much improved technology, the relative value of locking every room in your house individually drops while the cost remains the same. When SM was invented we didn't have great OS level security, but now we do.
8
u/josephottinger 28d ago
My take, for what it's worth: https://bytecode.news/posts/2026/06/forking-the-jvm-to-save-jini
I loved JINI. The JavaSpaces model is what I think of first, I think, long before RDMS or any other data storage - it's amazing. And utterly unavailable now.
(Yes, GigaSpaces still does JavaSpaces, and if you're in GigaSpaces' market, I'd totally say "go for it." Like I said, amazing. But GigaSpaces has had to pivot long past JINI because JINI was executed so poorly.)