9 ms·
I assume this is to prevent a file-dropping attack, similar to DLL injection. How hard is this to exploit in practice? Does javac include the current directory
by MarkSweep 2y ago
I assume this is to prevent a file-dropping attack, similar to DLL injection.
How hard is this to exploit in practice? Does javac include the current directory in the class path? Does it look in other directories that are easy for other users to drop files in?
Also, how much are people running javac directly? I would guess a lot of people use build tools like Gradle or Ant that limit the class path, right?
- senorrib 2y agoUsing Gradle, ant or Maven still means, in the end, you’re calling javac. All it takes for this to be exploited is for the files to be dropped anywhere in the classpath.
- jillesvangurp 2y agoIt's an annotation processor. They generally require annotations in your own source code to kick in. As a security risk this is pretty minor. About on the same level as any software project with any dependencies risking running arbitrary code unless you audit those dependencies. This is of course a very real risk and it has affected a bunch of projects. But it's not really stopping people from using things like cargo, npm, etc. Overall, it makes sense to make the use of annotation processors a bit more explicit. With Kotlin this is kind of how it works as well. You have things like ksp that you have to configure explicitly if you want to use them. Additionally there are compiler plugins that you can configure if you need them. It's not a big deal to configure this explicitly. I actually prefer it over magic discovery mechanisms that are hard to debug when they don't work. So, good change that probably simplifies the build process a little.
- vips7L 2y ago> They generally require annotations in your own source code to kick in Right but you don’t know which annotation processor will actually run. Anybody could look for javax.persistence.Entity and do something. There’s no guarantee only your JPA provider will be running and looking at them.
- jillesvangurp 2y agoIn exactly the same way, unless you audit your dependencies, you have no idea what you are going to run. It all boils down to whether you trust your dependencies. The only difference here is that this is a compile time dependency, not a run-time dependency. But unless you checked it, there are no guarantees.
- vips7L 2y agoSure. Hypothetically, I have checked them. I have checked what I’m actually using, but the compiler is still executing things I didn’t know about. I only use CatUtils from org.catpache.commons. I’ve audited this single class that I use. I know it’s safe. It only contains a Map<String, String> of Latin cat names to their common English name, but what I didn’t know was the compiler magically running an annotation processor behind my back and it’s now modified all of my classes to throw MeowException whenever toString returns “dog”.
- kaba0 2y agoIt’s pretty easy to statically verify that, say, no dependency (even transitive ones) contain a System.exit call. It’s basically impossible to determine what your annotation processors will output besides running them yourselves (and their output may not be deterministic to begin with).
- marginalia_nu 2y agoNot sure how much that would help. As far as I understand if you have access to putting stuff in the class path surely you can just override Java classes and run arbitrary code that way.
- derefr 2y agoI think the difference is that annotation processors run arbitrary code at compile time. An org might have e.g. a CI with a build environment that’s not as well-sandboxed as the test environment for the built app, because a Java compiler isn’t generally expected to (and other than through annotations, usually doesn’t) expose arbitrary code execution abilities to the payload of code being compiled.
- hedora 2y agoIs it actually common practice these days to have Java repositories that do not contain the build scripts, packaging, etc? The last time I looked at maven, it seemed easy enough to have it run arbitrary code directly during the build.
- kaba0 2y agoThis is mostly about supply chain attacks, where transitive dependencies are at play. Those are mostly just already built jar files, that can still contain annotation processors, but not build scripts.
- derefr 2y ago> Is it actually common practice these days to have Java repositories that do not contain the build scripts, packaging, etc? It's not that they don't contain these things; but rather that you can (and people often do) set things up so that the build scripts + packaging can be "more trusted" than the source files. If you've ever tried to set up CI on e.g. Github for an open-source project, then you might be familiar with the concept of a "PR attack" — where an external contributor forks your project, submits a PR that adds malicious code to your build scripts (to e.g. exfiltrate your build-time secrets); and then your CI "helpfully" runs those build scripts (in order to e.g. evaluate that the PR compiles + passes tests + lints in order to determine whether it should be blocked from merging or not.) GitHub and others have come up with ways around "PR attacks", that involve treating triggered automation workflows for external PRs differently: in these workflow runs, the core of the workflow — the workflow manifest file — is sourced not from the contributor's branch, but rather from the base branch that the PR aims to merge to. Now, it's up to you, as a repo maintainer, to come up with a way to bootstrap that little bit of safety (protected workflow manifest) into whole-repo "PR attack" arbitrary-code-execution protection. But usually doing so involves: 1. moving as much of the logic for executing your build as possible out of the repo itself and into buildpacks / "action" repos; and 2. sourcing any scripts you do need for the build, not from the worktree of the external PR, but rather by having the workflow environment also check out the base branch, and then running those scripts against your worktree. (IIRC the most common pattern for this is that you check out the base branch worktree, blow away its src/ dir, symbolic-link the PR branch's worktree's src/ into the base branch as src/, and then run your build in the context of the base branch.) This approach is incomplete, however, if the PR's source files can themselves be the source of arbitrary code execution.
- rzwitserloot 2y agoHow hard is this to exploit in practice? Very - this is a silly update in OpenJDK's war against things like Project Lombok. It _seems_ easy to exploit: Just.. get any jar file containing an annotation processor on the classpath and it will be executed as part of `javac` - and almost every java build tool calls javac under the hood. However, this is misleading: _if_ somebody with malicious intent manages to either sneak a jar file into the build dependencies somehow, or manages to libxz-style take control of a commonly used dependency, the damage is done. That jar will also be on the classpath when running the app. So now we're running compromised code. Java is not a sandboxed thing, running malicious code inside a JVM is a bit like an XSS web attack: The game is lost. Totally and utterly. The fact that javac runs annotation processors by default until JDK23 does mean that a compromised build chain now runs _during the build_, whereas starting with JDK23 they'll only run when you run the app. This doesn't seem impactful to me; I'm having a hard time figuring out scenarios where this is meaningfully less bad. Developers tend to run the app they are writing. The odds a developer will build a source tree and never actually run what they built, seems insufficient to consider this a meaningful contribution to security. CI servers might be a useful place to look for 'systems that will run the build but will not run the app', except - no. Just about every CI tool will run some tests, thus, running the app, thus, running the malicious code. Which gets us back to: OpenJDK's backwards-compatibility breaking crusade. The OpenJDK team has also broken reflection (you can no longer access anything in another module that wasn't explicitly exported without command line switches, even though reflection used to be able to do this. Reflection has 'CARE! You are accessing APis that were not designed to be messed with!' written on the tin. It's.. the point of it). - same reasoning. "For security" without being particularly clear about how that update contributes to security. It's not about 'it is impossible to use reflection to cause serious damage' (it is very possible to do that, in fact). It's more about: .... if you are running malicious code inside a JVM, we've got much, much bigger problems. It's sort of like stating that security is improved by ensuring that it is no longer possible to open the front door from inside the house without a key. Seems nice - but, they're... already inside the house.
- j16sdiz 2y ago> That jar will also be on the classpath when running the app. No. In many maven config, runtime classpath is not the same as compile time
- vbezhenar 2y agoI assume this is to have more control over compilation process. Without annotation processor, you can expect your code to be compiled and behaved in an obvious way. With annotation processor all bets are off, your code and code that results from compilation are completely different entities. So with this switch being explicit, you might enforce politics like lack of annotation processors for better clarity. While security theoretically might be better, in practice with modern build tools there are enough ways to cause code execution, so it probably doesn't matter much.