Using Rust Macros to exfiltrate secrets(github.com) |
Using Rust Macros to exfiltrate secrets(github.com) |
This can be done even easier without users having to use a macro: with `build.rs` build scripts, which are run by default. So all you'd need is to compromise some popular dependency with a custom build.rs
Many other languages have the same (or at least similar) problem (Makefiles, npm hooks, ...)
There is an interesting proposal and prototype for compiling proc macros to WASM so they can be run in a sandbox: https://github.com/dtolnay/watt
But in the end it doesn't make that much difference: nothing prevents a random library from just reading your secrets and calling curl to send it to a server at runtime.
Build time execution is definitely an additional attack vector.
But if you use a third party dependency, you have to trust it or review all it's code for every version. There is no way around this, and it's true for any language.
The difference here is that it happens when you open the project in the editor. If I'm suspicious of some code my first reaction would be to open it my editor and inspect it.
The ESLint extension always asks whether you trust the `eslint` executable before it's enabled. It's still quite easy to click "allow" without thinking about it, but at least you'll have a choice to not execute potentially random code.
I suspect the same problem exists in many other languages. How can you open a CMake project without executing it?
This simply isn’t true. All of these require an action by a user to execute the command (e.g npm install, make build). What the author is claiming is that a typical rust LSP setup will execute the arbitrary macro code simply by viewing the file in certain IDEs.
Feel free to show me an example of this in makefiles or npm and I’m happy to retract.
VS warns you with a confirmation dialog that shouldn't just be ignored because "I just want to look and not compile". So, don't open any random .csproj or .sln and assume you are safe.
Languages with similar risks are ones where a Repl is is the key form of development. In those scenarios you are also one bad dependency from stolen info.
The PoC doesn't even open a file, it just opens the directory. It's a pretty big difference, when you execute a build script you _expect_ to run code, when you open a directory in your editor you don't expect any side effect _at all_.
My guess is that since the proc_macros returns a TokenStream, rust-analyzer have no way to know what it provides except running it.
I'm not sure there's a solution for this that doesn't cripple macros in Rust, apart from being able to configure rust-analyzer to ignore the macros, which clearly limit its usefulness.
[1]: http://users.ece.cmu.edu/~ganger/712.fall02/papers/p761-thom...
A python plugin for an editor would have the same problem - if it imports a python module for any reason, like code completion. Same problem of arbitrary code execution.
I think we should work on solutions. Sandboxing both for editor plugins and for regular rust builds, should become the norm.
If your distro doesn’t enable SELinux, or your distro’s SELinux policy doesn’t protect your ssh keys, then you need to upgrade. If you don’t use Linux, then you need to upgrade to Linux.
It's a class of supply chain attack focusing on build time code evaluation. Almost every programming language has some kind of support for arbitrary code execution at build time, and any project of scale is going to require it.
RCE isn't an interesting exploit when the system is literally designed to run code from somewhere else.
This macro lets you embed an entire folder of assets in your binary at compile time, to simplify distribution.
Taking the concept further, I could also imagine build macros that compile Typescript or SASS files at build time, or generate data structures from a Protocol Buffers definition file, or in general operations that ingest non-Rust source code and use tools outside the repository.
Just installing a relatively popular crate (say Hyper) makes you realize that all of your secret could have been stolen by any of the myriad of dependencies.
https://internals.rust-lang.org/t/pre-rfc-procmacros-impleme...
I don’t think there’s an active working group though.
I can't see a robust solution to this, though.
It’s just not a new problem. Bash does auto–completion on Makefiles, which requires running make and asking it what the make targets are. IDEs can and will run ./configure for you, so that it can find the right include paths. Etc, etc.
Personally, I thought everyone already knew about this. I knew that proc macros would be a risk when I first heard about rls, years ago.
Certainly editors need to confirm with the user that they are ok with starting the compiler when they load a new project, but also we need to use fine–grained security systems like SELinux that can and do prevent programs from accessing things that they’re not supposed to access.
- During a session, the first time rust-toolchain encounters a proc macro it must run to analyze, it will first prompt the user and warn them.
- If the user accepts the prompt, rust-toolchain will freely run any proc macros until the next session.
- If the user rejects the prompt, that analysis will be disabled until the next session.
Similar to how VSCode and other apps handle opening links.
Safest way would probably be something hilarious like having the analyzer compiled to WASM and ran in node.js.
rust-analyzer.cargo.runBuildScripts (default: true)
Run build scripts (build.rs) for more precise code analysis.
https://rust-analyzer.github.io/manual.htmlJust as it turns out that matter and energy are almost the same thing seen from a different point of view, it's the same with code and data. Running code and processing data are no different to a computer.
You think a picture of a dog and a Windows program are plainly different kinds of things, the computer does not agree.
Something like Wuffs † aims to at least control the blast radius. If (in some alternate or far future world) you were only ever looking at pictures of a dog via Wuffs, you could at least feel confident that doing so did not have some entirely unforeseen consequences, like exfiltrating your SSH private keys. Today you certainly can't be sure of that, none of the tools you use have such a cautious approach.
VALUE := $(shell touch /tmp/something)¹ https://github.com/zsh-users/zsh/blob/master/Completion/Unix...
Many IDEs also do this for other languages (e.g. by running make), and the same problem applies.
https://www.jetbrains.com/help/idea/executing-build-file-in-...
(For example, Emacs realised that 'local eval' wasn't a good thing to have enabled globally in Emacs 19, in 1994, and spent the next decade or more closing many other loopholes involving local variables specified directly in files.)
If modern editors and IDEs are no longer thinking that way, I think that's a mistake.
Of course, I bet a lot of people don’t bother to read any of the source code of a program that they’ve downloaded anyway.
Same is true of `npm install`, deb/rpm/etc packages, etc: you don't have proof what was distributed to you matches up with what was in VCS.
You can read the code before it runs and solve the "curl could fail" theoretical arguments by just.. removing `| sh` and examining + running yourself.
Of course you can break the curl|sh into separate steps and check that the script isn’t malicious before you run it, but the fact that you have to do that makes it a bad idea to distribute software this way. If you were told to download an installation script, inspect it, and only then to run it then there would be less of a problem. curl|sh is yet another sign that we so often prefer convenience over reliability and safety.
Rust macros and build.rs require build commands to be executed by the rust compiler (cargo check, for example).
These are third party tools that have been implemented to execute build commands during initialization. It's not an issue with Rust, it's an issue with the implementation of the language client and text editor allowing the client to initialize when opening a workspace.
> Since that’s generally believed to be the case, no sandboxing is necessary.
That's where you're wrong. It's necessary even if you believe it's not. It's been proven time and time again that this is the case and that the "belief" no flaws exist is wrong.
Sandboxing approaches that use techniques like namespaces, and capability security have become vastly, vastly more popular over the years on Linux, and they're going to keep getting more popular, precisely because they work where SELinux fails (that is, 98% of the running Linux systems and distros that actually exist). Browsers, WebAssembly, systems like Flatpak with "Portals" -- all of them have moved into capability-inspired and "component" sandboxing approaches, to achieve this level of security independent of the host operating system. If Chrome had decided to use SELinux instead of its own sandboxing approach, it's security model would be completely inferior to what it is today.
They may be spiritually or metaphorically still build steps in some sense, but they are happening outside that context.