Contents
Summary
On Friday February 22nd we were notified of a critical security vulnerability through our responsible disclosure process. A fix was released as part of v1.0.2 three days later, on Monday Feb 25 and CVE-2019-9195 reserved. This document provides details on the vulnerability, the fix, and what measures we have taken to protect Grin users.
The vulnerability was discovered by the security firm we hired for our audit, Coinspect. It is related to the node syncing process and made it possible for a remote attacker to obtain write access to any part of the filesystem the node process had privileges to. This issue was fixed as part of the release of v1.0.2 and therefore we urge you to upgrade to v1.0.2 immediately. Users who are running this version or greater are not exposed. We have not seen any signs of the attack having been exploited, but we cannot know for sure that it has not been either.
At the time of this writing, Mainnet Grin is ~49 days old. Its simple design, minimal cryptographic assumptions, and implementation in Rust, avoid many pitfalls. And whilst user safety is a high priority for us and we do our best to ensure it, it’s important to underline that Grin is still highly experimental software that is bound to have many bugs.
Recommended action check-list
- Consider any environment that a v1.0.1 node or below was running in and had privileges to as potentially exposed.
- Boot up v1.0.2 nodes in a new environment.
- Never run grin as
root
.
Background
Third party disclosure
On Tuesday, Feb 26, the vulnerability was disclosed on keybase to known Grin exchanges, Grin mining pools, and selected MimbleWimble implementations, revealing that the vulnerability is related to the syncing process and makes it possible for a remote attacker to obtain file system write access to any part of the filesystem the node process has privileges to, and urging an upgrade to 1.0.2
Timeline of events
- Feb 22nd - responsible disclosure by coinspect
- Feb 25th - fix released with v1.0.2, CVE assigned
- Feb 26th - limited disclosure to mining pools and exchanges
- Mar 5th - Public disclosure
Technical details of CVE-2019-9195
The vulnerability makes it possible for a remote attacker to obtain file system write access to any part of the filesystem the node process has privileges to, and subsequent arbitrary code execution if a binary is replaced (for example the grin binary itself). This is caused by a Zip Slip in the extraction process of the zip that contains the blockchain state, necessary for a node to get synchronized with the latest chain.
To exploit the vulnerability, an attacker needs to provide a forged zip file to a Grin node that’s trying to synchronize itself with the latest state of the blockchain. This can happen either on first startup, or subsequently is a node is stopped for more than a few days and then restarted.
Detailed Fix Description
In short, remediating the vulnerability was done with 2 fixes:
- Detect paths that would result in creating a file outside of the directory
the zip is being extracted into and skip the corresponding zip file. - Filter files extracted from the zip from a whitelist of the data files we
expect in a Grin state archive.
The txhashset zip file obtained from peers should only contain paths
kernel/pmmr_data.bin
kernel/pmmr_hash.bin
rangeproof/pmmr_prun.bin
rangeproof/pmmr_leaf.bin.<blockhash>
rangeproof/pmmr_data.bin
rangeproof/pmmr_hash.bin
output/pmmr_prun.bin
output/pmmr_leaf.bin.<blockhash>
output/pmmr_data.bin
output/pmmr_hash.bin
where <blockhash>
is a block hash shortened to the first 12 hex digits, such as 0000045a7af3.
A bad zip file could however contain a path
../../../grin/target/release/grin
to try and overwrite the grin executable. The old zip::decompress
function in util/src/zip.rs
had no safeguard against such paths.
The fixed version in util/src/zip.rs has an additional 3rd argument string of expected filepaths,
which function expected_file
in chain/src/txhashset/txhashset.rs
computes as
format!(
r#"^({}|{}|{})(/pmmr_(hash|data|leaf|prun)\.bin(\.\w*)?)?$"#,
OUTPUT_SUBDIR, KERNEL_SUBDIR, RANGE_PROOF_SUBDIR
)
.as_str()
capturing the above file paths.
When iterating over all file paths in the zip file, the fixed code applies function sanitized_name
to each one. The zip-rs
crate documentation describes its function as:
Get the name of the file in a sanitized form. It truncates the name to the first NULL byte, removes a leading ‘/’ and removes
..
parts.
If its sanitized name differs from the filepath (as is the necessarily the case when escaping the target directory) or if the sanitized name doesn’t satisfy the regular expression, then we log
info!("ignoring a suspicious file: {}", file.name());
and skip the file.