Everything old is new again: binary security of WebAssembly
usenix.org
usenix.org
Notably, they don't appear to even try to break the WA-host memory barrier, which I actually find to be a validation of the core design goal of WebAssembly: isolate the damage a vulnerable program can inflict to the memory space (and thus also output) of that program. Protect the host from the program, but not the program from itself. Also, maybe don't dump WA output you can't validate directly into DOM.
Also the paper admits that security has been an important component of the design of wasm: "The design of WebAssembly includes various features to ensure binary security. For example, the memory maintained by a WebAssembly program is separated from its code, the execution stack, and the data structures of the underlying VM. To prevent type-related crashes and attacks, binaries are designed to be easily type-checked,which they are statically before execution. Moreoever, WebAssembly programs can only jump to designated code locations,a form of fault isolation that prevents many classic control flow attacks."
Because the shadow stack is created and managed by the WebAssembly binary itself, it would be its responsibility to add protections like stack protectors or ASLR on it within the linear memory if it wants them. A WebAssembly JIT isn't ever going to touch the linear memory in a way the binary doesn't specify.
>But function addresses can still be randomly offset, no?
No, functions have fixed indexes in a WebAssembly binary. You can't dynamically reassign the indexes at runtime.
You do have the benefit that if your program tries to jump to a function index chosen by an attacker, the attacker can only jump to a function with a compatible type signature. The attacker can't do anything too clever like jumping partway into an arbitrary function, jumping into attacker-written code in the mutable linear memory, or queuing up a series of return addresses to pull off return-oriented-programming.
> C++ compiled to WebAssembly generally manages its own parallel "shadow stack" in linear memory
In the paper, we call the compiler-organized stack in linear memory the "unmanaged stack", to differentiate it from the "evaluation stack" (WebAssembly is a stack-based VM, so this contains arguments and results of instructions) and the "managed call stack" (contains call frames, return addresses, local variables. Managed by the VM, cannot be inspected explicitly by WebAssembly instructions).
> attacker can only jump to a function with a compatible type signature
This is true, but note that WebAssembly types are fairly low-level. That is, there are only four primitive types (i32/i64, f32/f64) and, e.g., a C function that takes a string (char *) and a size_t would be type-compatible with a function that takes a signed int and a struct pointer (all those four types map to the same Wasm type: i32).
Say you are running OCR via some C library you have compiled to WASM: wrap that functionality up with an interface, compile that to WASM, and only pass it the image data you're happy for it to see. If someone exploits that binary with a malicious image, there's nothing inside the container except the code and the image itself, so hopefully the damage is contained.
If folks start compiling large amounts of their functionality into one single chunk of WASM, which has a complex interface with the DOM/JS, then I can see how these exploits could become an issue.
WebAssembly multi memory proposal and nanoprocesses (one of WASI initiative) should solve all this problems as well
That is good advice. Separately compiling C code into individual modules with small interfaces and as little host imports as possible does make exploitation harder. This is also one of the propositions we make in the mitigations section of the paper.
> If folks start compiling large amounts of their functionality into one single chunk of WASM
However, I also believe that is what will happen soon. Many people are eager to use WebAssembly for more parts of the front-end (and also because they want to develop in Go, Rust, etc., see the increasing number of DOM-libraries for WebAssembly). With more code being linked into a single WebAssembly module, it becomes more important to look into security inside a single WebAssembly module.
But unless you can break the sandbox isolation the worse you can get is what you get today with JsFuck, just faster.
I do agree with you, however, that our findings do not invalidate the overall design of WebAssembly. I really like the simplicity and generality of WebAssembly, and the technical (and social) achievement of having a "universal bytecode" supported in four different JavaScript engines (and some standalone VMs), on top of several different hardware architectures (x86, x86-64, ARM, etc.), with good performance, etc. I also think "host security" (which we are not really looking into) is solid, with two qualifications:
* In browsers, WebAssembly is sandboxed like JavaScript is, which is well tested.
* In other host environments (in particular, standalone VMs) the story might be different. It is hard to make any general statements about host security without talking about the concrete host at hand.
> they don't appear to even try to break the WA-host memory barrier
That's correct. However, just because we did not try, I would not say that breaking out of WebAssembly memory is impossible. But those attacks will be engine-specific. There have been some attacks against WebAssembly implementations (https://labs.f-secure.com/assets/BlogFiles/apple-safari-wasm..., https://googleprojectzero.blogspot.com/2018/08/the-problems-...) and surely will be more; a VM implementation is non-trivial with potential bugs after all.
> don't dump WA output you can't validate directly into DOM
Exactly, that is one of the problems with shortened statements such as "WebAssembly has great security". At the very least, one should be aware that inside WebAssembly's linear memory, many common security features are absent. This is relevant in particular when you link together large applications (including legacy C and C++ code) into a single WebAssembly binary, that all share the same linear memory.
> Designers of the WebAssembly spec explicitly chose to scope it's goal of a 'secure sandbox' to only include securing the host's memory from the WebAssembly VM. They have succeeded in this goal (modulo implementation deficiencies).
> However, they left open the question of how a developer might ensure the correctness of their program running in the sandbox. This paper explores the consequences of that original design goal for such a developer, and concludes that all the regular binary exploit mitigations may still be useful for ensuring program correctness.
I agree with your qualifications; sprinkling some WebAssembly on your app doesn't magically make security problems in the original code go away. If it's vulnerable it can still do damage to everything you give it access to.
So you leave your wallet, phone, and other critical devices in there. But now it has ceased to be a worry-free safe play space. It's no longer acceptable for the cat to shit in it.
This is a fundamental tension and we'll probably keep rediscovering this security problem any time a new container model is developed.
Proper, pervasive sandboxing in the architecture, ideally down to the function-level, would solve a huge fraction of security bugs, largely for free. Nobody would have had to invent Rust, because that 70% of memory safety bugs would have been almost entirely impossible to exploit from the start.
I mean, it's pretty obvious the tension here is largely imaginary when you consider how much random code browsers run from untrusted sources, day after day, and most of those times they fail are because the architecture and OS and language haven't got sensible de-facto sandboxes (again, see that 70% number). For sure, you need to be careful what you give access to who, but the problem is insufficient sandboxing, and the solution is more sandboxing.
Run a company that does security sandboxes of a type that you can deploy internally to see what kind of attacks are happening and potentially collect useful telemetry out of the attacker actions.
Users definitely want all the telemetry they can get, but also want to completely negate any risk of "accidental bad stuff happening." It's a tricky-to-balance equation, but also a fun and challenging problem to work on.
Sorry for being a bit late to the discussion, I will try to answer the questions in detail that were asked below.
I want to clarify one misunderstanding that seems to come up several times, namely the distinction between "host security" and "security of the WebAssembly program itself". WebAssembly does have measures and a good design for host security. E.g., in the browser, WebAssembly programs are run in a sandbox (just as JavaScript is), and writes inside WebAssembly's linear memory should never affect values outside of linear memory (e.g., VMs insert bounds checks when reading/writing from/to WebAssembly pointers). Those techniques protect against malicious WebAssembly binaries, which is of course important in the Web.
Here, we look at a different side of WebAssembly's security story: What if the WebAssembly binary is vulnerable and gets fed malicious input? In this attacker model, we can at most do what the host environment allows us to do. But especially for large WebAssembly programs with lots of imports, or WebAssembly binaries for standalone VMs (outside of the browser, without a tried-and-tested sandbox), this can still be a lot of attacker capability! And when we look into the protections inside WebAssembly's linear memory (not between linear memory and host memory), we find that there are very little. All linear memory is always writable, no stack canaries, no guard pages, no ASLR, no safe unlinking in smaller allocators etc. This is worrying as more code gets linked together into a single WebAssembly binary.
If you have any further questions, I am more than happy to answer, here and also via email. Thanks again for the interest!
In particular my understanding is that most of MVP WebAssembly almost a subset of asm.js in term of expressivity.
Do some of the safety measures you consider translate well in this model?
Yes, I think several mitigations could be deployed without requiring a change to the language, only by changing compilers and runtime libraries:
* Stack canaries on the unmanaged stack: For storing the reference canary value, you need some "safe" location that cannot be overwritten by regular memory writes. I believe in x86, thread local storage (TLS) / the fs segment register is used. In WebAssembly, one could use a non-mutable global scalar variable, which could only be overwritten by a matching global.set instruction (which cannot be inserted by an attacker). Then, before returning from a function (or really at any point you want to check the integrity of linear memory), you could compare the canary value against this global, as you would in native architectures.
* Allocator hardening, e.g., safe unlinking doesn't require any language support, the allocator just needs to do it (and live with the slightly increased code size cost).
Other mitigations would require language extensions. E.g., for finer grained control-flow integrity (taking source types into account, not just WebAssembly primitive types), one would require multiple table (part of the reference types proposal). Then, only functions with compatible types should be stored in the same indirect call table.
ASLR, unmapped pages, and constants in linear memory are harder, I believe. For ASLR, WebAssembly's 32-bit pointers will most likely provide too little entropy. Unmapped pages and "true constants" would require some host API to make portions of the memory non-writable/readable, but I don't know of any proposal to do so.
> wanting to keep implementation burden low for browsers
> WebAssembly almost a subset of asm.js
Yes, that is also my understanding as to why we are at the current situation. (But I did not take part in WebAssembly's design, so I cannot claim any authority.)
The selling point of WASM outside of browser to me is native modules for other languages where you can whitelist the exposed APIs (and provide sandboxed versions) while having cross-platform binaries.
So for example in the future node can ship with WASM module support and JS can load a C WASM module binary which I can deploy on Windows/Linux/Mac, and I can review what that module has access to via some module manifest, and node WASM exposes wrapped POSIX API based on manifest configuration.
* no corruption of, nor access to host memory.
* no corruption of, nor access to memory or other processes.
It looks like what they did is create a wasm file that generates a JS "Alert !!!" string, then blindly runs that string in the browser without any kind of validation?
It's hard to tell from looking at their code though:
https://github.com/sola-st/wasm-binary-security/blob/master/...
The html page there seems to be missing the "main.js" to see what's happening. :(
---
Hmmm, it might be this main.js, which looks (without checking) like it came from Emscripten:
https://github.com/sola-st/wasm-binary-security/blob/master/...
If you have a wasm application with vulnerabilities (e.g. in the libraries) there are no mitigations that native binaries provide, so simple buffer overflows give you RCEs again. It's still within the sandbox, but the threat is as severe as running eval on user supplied inputs as there might be useful stuff in that sandbox.
That aside, if the C-to-wasm compiler were to add some of the safeguards mentioned it does sound like it'd go some way towards lessening the potential impact.
The practical difference is that it's a lot harder to assure code written in unsafe languages is free of defects like this since they manifest as benign operations (every write to a buffer is a potential vector) rather than obviously dangerous operations. Concretely, you could grep for eval and convince yourself that each use is OK (assuming it's rare - it ought to be) but you couldn't do that for common language constructs that could be exploitable like writes to arrays/pointers.
You are right in that we do not try to attack a concrete host implementation or aim to break out of the sandbox.
I disagree, however, that security inside a WebAssembly binary is irrelevant. If an attacker can arbitrarily read/write in linear memory, it depends on the imported host functions what can be done. The larger WebAssembly applications are (and we believe they will become larger in the future, and incorporate DOM functions, as in our PoC application), the higher the risk that host APIs can be abused. This is troubling especially where there is no sandbox (on some standalone VMs), but even in browsers (where it opens the door for a new type of cross-site scripting).
For example, even if you only call JS eval() with a constant string in your C code, because WebAssembly has no truly constant memory, this can give XSS to an attacker with a memory write primitive inside linear memory.
> The html page there seems to be missing the "main.js" to see what's happening. :(
Oops, good point. They are now pushed, sorry about that.
> main.js, which looks (without checking) like it came from Emscripten
That's correct, you can also check the build script: https://github.com/sola-st/wasm-binary-security/blob/master/...
On one hand, the problem doesn't seem to be any different than happens with standard JS. The exploit was possible only because the wasm (literally) did no input validation. And "validate all input" is the first thing web programmers learn. (or very close to first ;>)
On the other hand, it is a vector. And we've seen plenty of cases where vectors are chained together in novel ways to enable unexpected attacks. So there's that. ;)
> Oops, good point. They are now pushed, sorry about that.
No worries, thanks for getting that done. :)
WASM seems like very good solution if you want to load random code and still retain as much safety and flexibility as possible. The burden is now moved to code that interacts with potentially misbehaving code. Check and verify all outputs, limit resource usage. Using WASM system It's not so different from calling remote server in the cloud. Anything can happen during executing but only the interacting with the system can cause harm outside the system.
When they exist, potential WASM side channel attacks are system/compiler specific and can be fixed with software updates. Compilers can be designed to remove the possibility it becomes a real issue.
That's extremely wrong. People have been hand writing WASM files for ages.
As an experimental feature... I had to use it on one of the projects I work on because using signed bytes was driving me crazy, but I had to suppress warnings for experimental features (and will have to pay the price if they change the feature somehow, as they tend to do with experimental features).
CLR also has an object model and a memory model that's not very friendly to unmanaged languages, hence C++/CLI, and the runtime is not very lightweight for embedding purposes
It has been aggresively marketed as a JVM alternative (not equivalent), but with the same old native bugs and even makes XSS attacks possible. Stunning.
Maybe that's why all the big media content companies went behind it. Out with Flash and Java and in with 'compile and run WASM programs in every browser' to support this 'open web™'.
What they really mean is: 'We can't wait for WASM DRM!'
Guess what: developers are still responsible for avoiding security bugs in their code. But at least when they mess up, it still won't damage more than what their dumb web app could have done already.
And we start the cycle over again around the time that young men and women no longer trust the opinions of people who lived through the last cycle.
I used to call this 'fashion' as quite a few other people are fond of doing, but I don't think I'll be doing that anymore. Fashion has an element of frivolity that understates the consequences of all of the churn.
I have been trying to figure out if there's a more structured way that I can gather my assumptions (about software in particular) and be able to audit them once in a while. Because we build so many decisions on these assumptions. And I suspect when they start to teeter is when we get the urge to try something very different, 'forget everything we think we know' and start over with a philosophy that we already tried and abandoned thirty years ago.
But maybe I only need to forget six things I 'knew', instead of swinging back and forth on the pendulum with everyone else.