Why is JavaScript "safe" to run in the browser?
The standard is designed to be safe. The implementation may not be.
The browser isolates JavaScript, as it executes within a browser process itself. It cannot do anything which is not permitted by the browser JavaScript interpreter or JIT compiler. However, owing to its complexity, it's not at all uncommon to find vulnerabilities that allow JavaScript to compromise the browser and gain arbitrary code execution with the privileges of the browser process.
Because these types of security bugs are so common, many browsers will implement a sandbox. This is a protection mechanism that attempts to isolate a compromised browser process and prevent it from causing further harm. The way the sandbox works depends on the browser. Firefox has very limited sandboxing, whereas Chrome and Edge have significant sandboxing. However, despite this defense in depth, browser vulnerabilities can often be combined with sandbox escape vulnerabilities.
How is it safe?
It is not. Or more exactly it is as safe as the browser implementation is. Browsers (including their JavaScript engine) are complex pieces of software, with regular addition of new features - because users want them.
That means that, even if well-known ones certainly have quality procedure to test their code against known vulnerabilities, the risk of an undetected flaw in implementing a feature always exists.
Currently, the accepted way is that as soon as a breach is detected, a new version containing a fix is released. But between the discovery of the breach and the installation of the fix, the browser is vulnerable.That is the reason why it is recommended to keep one's browser up to date, in order to only be exposed to zero-day vulnerabilities.
It's true that at the JavaScript level the browsers are designed to sandbox the code under execution (primarily by not exposing any dangerous API), but JavaScript is a very complex language to parse and execute.
ECMAScript is the standard behind JavaScript, due to the huge marketing inflation around beginner-friendly programming languages we are experiencing today, the ECMAScript is updating fast and introducing more and more complex functionality to implement for a runtime.
This, in turn, widen the surface of attack and allow bugs to creep in.
A wonderful example is the recent work of Patrick Biernat, Markus Gaasedelen, Amy Burnett for the pwn2own 2018.
http://blog.ret2.io/2018/06/05/pwn2own-2018-exploit-development/
The blog describe the discovery of a 0day that allows arbitrary code execution in WebKit and how that's used to exploit another 0day in the macOS Window Manager to escape the sanbox Safari is running in (that's a macOS sandbox, not a Safari's one) to escalate to "root" and own the system.
Long story short, by simply visiting a link while having JavaScript enabled, a macOS system can be totally compromised without a single glitch visible to the user.
That's how safe JavaScript can be: As safe as a complex piece of software as WebKit's JSCore can be.
That's why users that require high security are advised to disable JavaScript (that's a fairly common requirement in the DarkWeb, for example).
The vulnerability in WebKit discovered by the authors above is a race condition between the newly introduced garbage collector and the array.reverse
function: if the GC starts marking an array while it is being reversed it could lead to a UAF (Use After Free) exploit.
The mark is done sequentially on the array, suppose the GC is right at the middle of the array when this is reversed, then second half is never marked and thus elected for collection, resulting in a UAF (the array object itself is not collected, only its element).
How an UAF is used to make more powerful exploit primitives that can lead to arbitrary code execution is more or less a variation of the same techniques: first an interesting object is allocated in the newly freed space (e.g. an array) then a RW primitive is created (e.g. by altering the boundary of the array) and finally arbitrary opcodes are written in memory (e.g in a JITted page).
The details for this particular 0day are in the linked blog.
The interesting part is the way this 0day was found: since WebKit is so big an in depth code review is impossible without an huge amount of effort so and automated jitter was set up.
This must make us reflect on the fact that when we have hundred thousand or millions of lines of code, it's very hard to make each one behave nicely with respect to every other one.