JEP 411 Headaches: Instrumenting private methods in the JDK for authorization checkpoints.
Peter Firmstone
peter.firmstone at zeus.net.au
Mon Jul 26 01:05:32 UTC 2021
Hi Alan,
My apologies, finalizer removal, that's a positive step, I thought it
was motivated by "the code is trusted so we don't need to worry about
finalizer attacks".
Also apologies for the long email, but it seems appropriate for
information handover.
Atomic construction guarantee; if invariant checks during construction
throw an exception, prior to calling Object's zero arg constructor, an
object instance will not be created, creation of the object is atomic,
either it succeeds with all invariants satisfied, or an object is not
created, such that it cannot be created in a broken state where
invariants aren't satisfied. We apply this constructor safety guarantee
to deserialization of data, it is a requirement of the code performing
deserialzation to check invariants and we require that users are
authenticated, this is to avoid parsing untrusted data, while still
validating user data. We enforce it using SM infrastructure.
I understand JEP 411 is a business decision, there wasn't enough
adoption, following the fallout of applets, businesses and users running
afoul of untrusted code, causing ongoing pain (publicly). The remaining
attempts of JEP 411 to explain why POLP is a technical failure only
apply to the default implementation and are incorrect when applied to
other implementations, it's a commercial failure, as suggested by low
rates of adoption in JEP 411, but that's due to a lack of investment in
tooling, however I suspect OpenJDK has underestimated adoption, although
probably not by a big margin, but I suspect it will be more painful than
OpenJDK anticipates. I have a perfectly good working reliable publicly
available example (for years) contrary to JEP 411's technical claims.
OpenJDK's decision has been made, and those affected must also assess
and make their own decisions, the following only serves to share my
thoughts and insights, no need to read further if not of interest. Our
Java programs are going into care and maintenance as we assess suitable
replacement development platforms.
<--------->
Applets relied on SM (perhaps SM only exists due to their success),
applets themselves weren't the cause of their own demise, for that we
have Java Serialization to thank, otherwise applets were a commercial
success, and had they remained so, then SM would have also remained, it
appears to be inexorably tied to the fate of applets now.
Serialization needed an atomic replacement before 2008, when it was
becoming obvious that Java serialization was insecure. OpenJDK could
still fix Java serialization without using white list filters
(ironically white listing is a complication of SM, which reduced
adoption, it's likely the same will occur with Serialization white
lists, if tooling isn't provided), by removing the ability to serialize
circular object graphs, or disabling it by default. We had circular
object graphs in JGDMS (which heavily utilised Java serialization), but
we refactored these out after implementing atomic de-serialization, we
did this in a way that didn't require breaking the serial form
compatibility of existing classes (unless they contained circular
references). This keeps serial data invariant validation code with the
object implementation, rather than as a separate white list (and it is
more powerful and less complex than white listing), reducing complexity
and maintenance, and because failure is atomic an attacker cannot
formulate a gadget chain, type safety is also read ahead and checked
prior to de-serialization of data. The development of atomic
serialization started with atomic deserialization which was completed a
few years ago, atomic serialization was under current development, with
new explicit public API methods for used for serialization, to avoid any
issues with reflection and module access controls, we were still using
Java serialization to serialize, but an alternative
AtomicObjectInputStream to de-serialize.
SM Performance isn't an issue, my policy implementation is high scaling
and has no hotspots, neither is deployment, we have tools to generate
policy files (more than one) and have been doing so for many years (the
first tool was written by Sun Microsystems circa 2004 I think, it still
required policy file editing, but listed permissions required), the
second tool was written 8 years ago approx. Our use cases have passed
the tests of time. I don't believe people hand author policy files in
the age of computing: I've seen examples of policy generation tools by
other authors on GitHub. Sure some developers might grant
AllPermission, to get something running, or for tests, but I haven't
seen anyone serious about security in production that does. I don't use
the built in policy provider (it has a blocking permission cache that
negatively impacts performance), my policy implementation doesn't have a
cache and it's many magnitudes faster and high scaling thanks to shared
immutability, thread confined mutability and the garbage collector, the
last remaining pain point is SocketPermssion and DNS. If
SocketPermission was changed to use URI RFC3986 normalization and had
netmask wildcards, that would address remaining issues (Too many
SocketPermission grants required in policy files). I managed to avoid
most DNS calls by using a PermissionComparator, which finds the closest
match, if it doesn't find an exact match, to reduce the number of actual
permission checks. We also had to implement our own ClassLoader to
avoid the DNS calls originating from SecureClassLoader.
Java's memory model has more voodoo than AccessController, the JMM makes
AccessController look simple, programming is voodoo to a non programmer,
everything is relative and based on experience. I guess you are
suggesting the complexity to use ratio is too high. I can imagine what
it was like being at a presentation when someone enables SecurityManager
and it stops working, why the default provider wasn't fixed then I'm not
sure, budget perhaps? If I was at that presentation, I'd generate the
policy file on the first pass, then make the new policy file the default
policy. It would have added about 10 minutes to the presentation as we
stepped through the swim lanes, or whatever people like to call
executing desired functionality (preferably automated test cases), at
least it would look like we knew what we were doing, then you validate
it by attempting to do something you shouldn't, it leaves the JVM in a
very locked down state (which can also prevent user errors),
occasionally you can forget to execute some necessary functionality (not
have a test case for it), or not allow the program to run long enough to
capture all necessary permissions, but the missing permissions are
quickly sorted, by running the tool a second time, the missing
permissions are appended to policy files.
I would have preferred that we reduced the number of permissions, these
can be removed without breaking backward compatibility, the policy just
treats it as an UnresolvedPermission.
Many permissions are being replaced by better access controls (as they
should), such as more recent changes to package / module access and
reflection and this could be a gradual process as unnecessary
permissions are removed. We aren't trying to sandbox code, it's used
for company authorization rules. The code is trusted, but if it's from
a third party, or another company with a business relationship, eg
approved vendor, we need to place some constraints on it, which aren't
intended to defend against malicious code. Our intent is to prevent
parsing of malicious data and loading of malicious code.
I would have preferred to build a model around permissions required for
authorization of users and trusted code, rather than focusing on
sandboxes and malicious code. If a user or service doesn't
authenticate, they cannot dynamically load code because they are not
granted permission to do so, they also cannot generally deserialize
untrusted data, this all depends on SM infrastructure.
We need to control access to networks, files, user credentials,
properties (of certificate store locations) and class loading. Primarily
we do this by authenticating users, we also allow some authenticated
users and services to also dynamically download and load classes.
Due to JEP 411, we have no future Java upgrade path, what's possible
currently with authorization, will not be possible in future versions of
Java, design assumptions throughout our software are built on SM
infrastructure. When we remove SM from our systems, it enables
deserialization of untrusted data from unauthenticated users and it
allows downloads and class loading of untrusted code. Our software
uses TLS over IPv6 for it's point to point connectivity and is able to
dynamically discover compatible services over global networks, so we
cannot use a firewall to protect it either.
The library we use and provide publicy can be found here, in case its of
interest: https://github.com/pfirmstone/JGDMS
At the end of the day, choosing any development platform is a risk, and
this was one of the risks of choosing a commercial platform (at that
time Java was a commercial platform and it's still funded by commercial
interests today), budgets dictate both the initial development
compromises and the eventual demise when adoption falls and a feature is
no longer commercially viable for the people responsible for maintaining
it.
Due to JEP 411 all our Java development will be put into care and
maintenance, I'm currently looking at our options for other programming
languages and platforms on which to base new development, Haskell looks
interesting and seems to have better type safety, due to it's academic
background, its developers are focused on solving very difficult
problems and doing so in a way that is provably correct, using
mathematical theorems, I think that's why its taken years longer to
stabilize. By not having made compromises, it will likely be useful for
much longer, even with some change. It seems unlikely that an academic
language would lose features due to budget constraints, it is more
likely that inadequate or problematic features will be addressed and
upgraded or replaced.
It is not so much that JEP 411 might break backward compatibility, we
can live with that, what we are unable to address; it removes a feature
that cannot be re-implemented and has no replacement, which exposes us
to low probability, but unacceptable consequences.
There are no hard feelings, it's just a consequence of our original
platform adoption choice, we knew there were risks. It's time to move
on and deal with it. No doubt Java will be useful to many people for
many years to come, and many don't require an authorization layer or
chose something other than SM to implement one. With no Java upgrade
path, it leaves us free to choose from what is available now, rather
than a choice made 20 years ago. In any case it's likely a choice that
we would have needed to make eventually, JEP 411 has only brought it
forward. If Haskell is a magnitude more efficient, as its proponents
claim, then it may ultimately provide an overall cost saving. We
haven't made a choice yet though, it's still under investigation.
I do appreciate that you took the time to respond to my emails.
Regards,
Peter.
On 26/07/2021 12:44 am, Alan Bateman wrote:
> On 23/07/2021 23:33, Peter Firmstone wrote:
>> I think it's worth noting that there isn't a way to securely run code
>> with malicious intent now, so I'm surprised that at this late stage
>> you were still providing support for sand boxing (whack a mole).
>>
>> It's just for us many assumptions have been made on a Java platform
>> with SM, using POLP (not sandboxing) as this was one of the
>> foundational principles of secure coding guidelines (just like
>> following concurrency best practice, were were following security
>> best practice). Sandboxing is an all or nothing approach, if you
>> had a trusted applet that was signed, it had AllPermission, if you
>> had an unsigned applet, then it had no permissions. Sandboxing was
>> one of the use cases for SM, when combined with ClassLoader
>> visibility, but we never realized that OpenJDK developers meant
>> sandboxing == authorization access controls.
>>
>> When you remove that pillar, everything it's supporting collapses,
>> not just sand boxing, so when you say you are removing support for
>> sandboxing, we say, good idea, but we didn't realize you were saying
>> you were removing support for all authorization access controls.
>> Reduced and revised authorization and access control would have been
>> acceptable, as tightening reflection visibility using a different
>> form of access control removes the need for authorization based
>> reflection access checks, but also removing atomic construction
>> guarantee's just seems like were doing this at a rapid pace without
>> the community understanding what you have in mind, and this may have
>> more uses than just stopping finalizer attacks.
> I'm not 100% sure what you mean by "atomic construction guarantee"
> here. This JEP does not propose to change anything with finalization
> or do anything with the registration of finalizers after Object.<init>
> runs. Our exchange in the previous mails was about classes (using
> ClassLoader as the example) that specify a SM permission check in
> their constructors, something that is strongly discouraged as the
> checks are easy to bypass. The idiom that we use in the JDK to prevent
> bypassing these SM permission checks with a finalizer attack is to
> check in a static method that returns a dummy parameter for the
> invokespecial. My point in the previous mail is that when the SM
> permission checks eventually go away then many of the uses of this
> idiom can do away too.
>
> That said, there is strong desire to eventually remove finalization
> too. Finalization was deprecated several years ago and the Java
> platform defines APIs that provide much more flexible and efficient
> ways do run cleanup actions when an object becomes unreachable. So
> another multi-year/multi-release effort to remove a problematic
> feature, just nothing to do with this JEP.
>
> As regards POLP, the focus of SM architecture when it was enhanced in
> Java 1.2. The JEP attempts to explain why this has been a failure.
> AccessController is voodoo that most developers have never encountered
> so anyone trying to run with SM ends up running counter to the
> principle of least privilege by granting all permissions.
>
> -Alan.
More information about the security-dev
mailing list