The Rule of 2
If there were an input type so simple that it were straightforward to write a memory-safe handler for it, we wouldn‘t need to worry much about where it came from for the purposes of memory safety, because we&rsquo…
以下正文同步自 Hacker News 最新,版权归原站所有,已转换为易读排版。
If there were an input type so simple that it were straightforward to write a memory-safe handler for it, we wouldn‘t need to worry much about where it came from for the purposes of memory safety, because we’d be sure we could handle it. We would still need to treat the input as untrustworthy after parsing, of course.
Unfortunately, it is very rare to find a grammar trivial enough that we can trust ourselves to parse it successfully or fail safely. (But see Normalization for a potential example.) Therefore, we do need to concern ourselves with the provenance of such inputs.
Any arbitrary peer on the Internet is an untrustworthy source, unless we get some evidence of its trustworthiness (which includes at least a strong assertion of the source's identity). When we can know with certainty that an input is coming from the same source as the application itself (e.g. Google in the case of Chrome, or Mozilla in the case of Firefox), and that the transport is integrity-protected (such as with HTTPS), then it can be acceptable to parse even complex inputs from that source. It's still ideal, where feasible, to reduce our degree of trust in the source — such as by parsing the input in a sandbox.
Unsafe implementation languages are languages that lack memory safety, including at least C, C++, and assembly language. Memory-safe languages include Go, Rust, Python, Java, JavaScript, Kotlin, and Swift. (Note that the safe subsets of these languages are safe by design, but of course implementation quality is a different story.)
Some memory-safe languages provide a backdoor to unsafety, such as the unsafe keyword in Rust. This functions as a separate unsafe language subset inside the memory-safe one.
The presence of unsafe code does not negate the memory-safety properties of the memory-safe language around it as a whole, but how unsafe code is used is critical. Poor use of an unsafe language subset is not meaningfully different from any other unsafe implementation language.
In order for a library with unsafe code to be safe for the purposes of the Rule of 2, all unsafe usage must be able to be reviewed and verified by humans with simple local reasoning. To achieve this, we expect all unsafe usage to be:
Because unsafe code reaches outside the normal expectations of a memory-safe language, it must follow strict rules to avoid undefined behaviour and memory-safety violations, and these are not always easy to verify. A careful review by one or more experts in the unsafe language subset is required.
It should be safe to use any code in a memory-safe language in a high-privilege context. As such, the requirements on a memory-safe language implementation are higher: All code in a memory-safe language must be capable of satisfying the Rule of 2 in a high-privilege context (including any unsafe code) in order to be used or admitted anywhere in the project.
High privilege is a relative term. The very highest-privilege programs are the computer‘s firmware, the bootloader, the kernel, any hypervisor or virtual machine monitor, and so on. Below that are processes that run as an OS-level account representing a person; this includes the Chrome Browser process and Gpu process. We consider such processes to have high privilege. (After all, they can do anything the person can do, with any and all of the person’s valuable data and accounts.)
Processes with slightly reduced privilege will (hopefully soon) include the network process. These are still pretty high-privilege processes. We are always looking for ways to reduce their privilege without breaking them.
Low-privilege processes include sandboxed utility processes and renderer processes with Site Isolation (very good) or origin isolation (even better).
Turning a stream of bytes into a structured object is hard to do correctly and safely. For example, turning a stream of bytes into a sequence of Unicode code points, and from there into an HTML DOM tree with all its elements, attributes, and metadata, is very error-prone. The same is true of QUIC packets, video frames, and so on.
Whenever the code branches on the byte values it's processing, the risk increases that an attacker can influence control flow and exploit bugs in the implementation.
Although we are all human and mistakes are always possible, a function that does not branch on input values has a better chance of being free of vulnerabilities. (Consider an arithmetic function, such as SHA-256, for example.)
Chrome Security Team will generally not approve landing a CL or new feature that involves all 3 of untrustworthy inputs, unsafe language, and high privilege. To solve this problem, you need to get rid of at least 1 of those 3 things. Here are some ways to do that.
Where possible, it's great to use a memory-safe language. The following memory-safe languages are approved for use in Chromium:
One can imagine Kotlin on Android, too, although it is not currently used in Chromium.
For an example of image processing, we have the pure-Java class BaseGifImage. On Android, where we can use Java and also face a particularly high cost for creating new processes (necessary for sandboxing), using Java to decode tricky formats can be a great approach. Before switching to a Rust-based parser, we used a Java JsonSanitizer, to ‘vet’ incoming JSON in a memory-safe way before passing the input to the C++ JSON implementation.
On Android, many system APIs that are exposed via Java are not actually implemented in a safe language, and are instead just facades around an unsafe implementation. A canonical example of this is the BitmapFactory class, which is a Java wrapper around C++ Skia. These APIs are therefore not considered memory-safe under the rule.
The QR code generator is an example of a cross-platform memory-safe Rust library in use in Chromium.
Also known as sandboxing, privilege reduction means running the code in a process that has had some or many of its privileges revoked.
When appropriate, try to handle the inputs in a renderer process that is Site Isolated to the same site as the inputs come from. Take care to validate the parsed (processed) inputs in the browser, since only the browser can trust itself to validate and act on the meaning of an object.
Equivalently, you can launch a sandboxed utility process to handle the data, and return a well-formed response back to the caller in an IPC message. See Safe Browsing's ZIP analyzer for an example. The Data Decoder Service facilitates this safe decoding process for several common data formats.
If you can be sure that the input comes from a trustworthy source, it can be OK to parse/evaluate it at high privilege in an unsafe language. A “trustworthy source” means that Chromium can cryptographically prove that the data comes from a business entity that you can or do trust (e.g. for Chrome, an Alphabet company).
Such cryptographic proof can potentially be obtained by:
Pinned TLS needs to meet all these criteria to be effective:
It is generally preferred to use Component Updater if possible because pinning may be disabled by locally installed root certificates.
One common pattern is to deliver a cryptographic hash of some content via such a trustworthy channel, but deliver the content itself via an untrustworthy channel. So long as the hash is properly verified, that's fine.
You can ‘defang’ a potentially-malicious input by transforming it into a normal or minimal form, usually by first transforming it into a format with a simpler grammar. We say that all data, file, and wire formats are defined by a grammar, even if that grammar is implicit or only partially-specified (as is so often the case). A data format with a particularly simple grammar is SkPixmap. (The ‘grammar’ is represented by the private data fields: a region of raw pixel data, the size of that region, and simple metadata (SkImageInfo) about how to interpret the pixels.)
It's rare to find such a simple grammar for input formats, however.
For example, consider the PNG image format, which is complex and whose C implementation has suffered from memory corruption bugs in the past. An attacker could craft a malicious PNG to trigger such a bug. But if you transform the image into a format that doesn‘t have PNG’s complexity (in a low-privilege process, of course), the malicious nature of the PNG ‘should’ be eliminated and then safe for parsing at a higher privilege level. Even if the attacker manages to compromise the low-privilege process with a malicious PNG, the high-privilege process will only parse the compromised process' output with a simple, plausibly-safe parser. If that parse is successful, the higher-privilege process can then optionally further transform it into a normalized, minimal form (such as to save space). Otherwise, the parse can fail safely, without memory corruption.
The trick of this technique lies in finding a sufficiently-trivial grammar, and committing to its limitations.
正文由 探How 从来源站点 RSS 同步,内容未经改写;遇到排版缺失或需要图片、视频时请以原文为准。