Summary
sha2 is pinned at 0.10.9 here. When it moves to 0.11 (Dependabot will propose it), the
build support crate stops compiling: the font checksum is rendered with {:x}, which 0.11
removes.
This one also has an unbounded network read worth fixing at the same time, since the code is
already being touched.
Affected site
build_support/src/font.rs:172 — let actual = format!("{digest:x}");
Hardening: bound the response body
fetch_font_data() currently does:
let bytes = resp
.bytes()
.with_context(|| format!("reading response body from {FONT_URL}"))?;
resp.bytes() buffers the entire response into memory with no cap, before the checksum is
checked. A hostile or misbehaving origin at that URL can therefore drive build-time memory use
arbitrarily high. Since this runs in a build script, the failure lands on every developer and
CI job.
Cap it explicitly. With reqwest's blocking API:
use std::io::Read;
/// A Fira Sans regular TTF is ~400 KiB; this leaves generous headroom while
/// bounding what a hostile origin can make the build allocate.
const MAX_FONT_BYTES: u64 = 8 * 1024 * 1024;
let mut reader = resp.take(MAX_FONT_BYTES + 1);
let mut bytes = Vec::new();
reader
.read_to_end(&mut bytes)
.with_context(|| format!("reading response body from {FONT_URL}"))?;
if bytes.len() as u64 > MAX_FONT_BYTES {
return Err(anyhow!(
"font exceeds the maximum of {MAX_FONT_BYTES} bytes"
));
}
Reading MAX + 1 and then checking is what distinguishes "exactly at the cap" from
"truncated silently at the cap" — a plain take(MAX) cannot tell those apart.
What does not apply here
The expected digest is a compile-time constant (FONT_SHA256), not a fetched .sha256
sidecar. So the sidecar-specific hardening from the whitaker migration — validating the token
is 64 hex digits, lowercasing before comparison, capping the sidecar read — is not relevant
to this repo. Likewise, tmp.persist(&font_path) already gives you an atomic publish, so the
partial-artefact cleanup work does not apply either.
The comparison at line 173 is already constant-to-constant lowercase, so it stays as is.
The fix
sha2 0.11 returns hybrid_array::Array<u8, _> from finalize()/digest(). That type
derefs to [u8] but does not implement core::fmt::LowerHex, so {:x} no longer
compiles. Render the digest explicitly instead.
If this crate already depends on hex, hex::encode(digest) is sufficient. Otherwise add a
small crate-internal encoder — no new dependency needed:
/// Encode `bytes` as a lowercase hexadecimal string.
///
/// Every byte renders as exactly two digits, including leading zeroes, so the
/// output is always twice the input length.
#[must_use]
fn to_lower_hex(bytes: &[u8]) -> String {
const HEX_DIGITS: &[u8; 16] = b"0123456789abcdef";
let mut hex = String::with_capacity(bytes.len() * 2);
for &byte in bytes {
hex.push(char::from(HEX_DIGITS[usize::from(byte >> 4)]));
hex.push(char::from(HEX_DIGITS[usize::from(byte & 0x0f)]));
}
hex
}
Call sites become to_lower_hex(&hasher.finalize()) — the & coerces Array to &[u8]
via Deref.
Worth a bounded test over the whole u8 range: each byte must render as exactly two
lowercase hex digits that parse back to the original value. That catches leading-zero bugs
a couple of example vectors would miss.
Also in this family
sha2 0.11 does not implement std::io::Write either, so io::copy(&mut reader, &mut hasher)
stops compiling. I did not find that pattern in this repo, but if one appears, use a bounded
buffered read loop or a small io::Write adapter newtype that forwards to hasher.update().
The sibling RustCrypto crates move in lockstep and carry the same break — sha1/sha3/md-5
go to 0.11, and hmac/hkdf/pbkdf2 go to 0.13.
Validation
cargo fmt --all -- --check
cargo clippy --workspace --all-targets --all-features -- -D warnings
cargo test (or the repo's make test)
Reference
Worked example: leynos/whitaker#296, which migrated the installer crate and added the
hardening described above. installer/src/hex.rs there is the encoder; the digest call sites
are in installer/src/dependency_binaries/install/checksum.rs.
Raised from an estate-wide survey of sha2 exposure. Sites were identified statically
(manifest and source inspection); they have not been compiled against 0.11.
Summary
sha2is pinned at0.10.9here. When it moves to0.11(Dependabot will propose it), thebuild support crate stops compiling: the font checksum is rendered with
{:x}, which 0.11removes.
This one also has an unbounded network read worth fixing at the same time, since the code is
already being touched.
Affected site
build_support/src/font.rs:172—let actual = format!("{digest:x}");Hardening: bound the response body
fetch_font_data()currently does:resp.bytes()buffers the entire response into memory with no cap, before the checksum ischecked. A hostile or misbehaving origin at that URL can therefore drive build-time memory use
arbitrarily high. Since this runs in a build script, the failure lands on every developer and
CI job.
Cap it explicitly. With
reqwest's blocking API:Reading
MAX + 1and then checking is what distinguishes "exactly at the cap" from"truncated silently at the cap" — a plain
take(MAX)cannot tell those apart.What does not apply here
The expected digest is a compile-time constant (
FONT_SHA256), not a fetched.sha256sidecar. So the sidecar-specific hardening from the whitaker migration — validating the token
is 64 hex digits, lowercasing before comparison, capping the sidecar read — is not relevant
to this repo. Likewise,
tmp.persist(&font_path)already gives you an atomic publish, so thepartial-artefact cleanup work does not apply either.
The comparison at line 173 is already constant-to-constant lowercase, so it stays as is.
The fix
sha20.11 returnshybrid_array::Array<u8, _>fromfinalize()/digest(). That typederefs to
[u8]but does not implementcore::fmt::LowerHex, so{:x}no longercompiles. Render the digest explicitly instead.
If this crate already depends on
hex,hex::encode(digest)is sufficient. Otherwise add asmall crate-internal encoder — no new dependency needed:
Call sites become
to_lower_hex(&hasher.finalize())— the&coercesArrayto&[u8]via
Deref.Worth a bounded test over the whole
u8range: each byte must render as exactly twolowercase hex digits that parse back to the original value. That catches leading-zero bugs
a couple of example vectors would miss.
Also in this family
sha20.11 does not implementstd::io::Writeeither, soio::copy(&mut reader, &mut hasher)stops compiling. I did not find that pattern in this repo, but if one appears, use a bounded
buffered read loop or a small
io::Writeadapter newtype that forwards tohasher.update().The sibling RustCrypto crates move in lockstep and carry the same break —
sha1/sha3/md-5go to 0.11, and
hmac/hkdf/pbkdf2go to 0.13.Validation
cargo fmt --all -- --checkcargo clippy --workspace --all-targets --all-features -- -D warningscargo test(or the repo'smake test)Reference
Worked example: leynos/whitaker#296, which migrated the installer crate and added the
hardening described above.
installer/src/hex.rsthere is the encoder; the digest call sitesare in
installer/src/dependency_binaries/install/checksum.rs.Raised from an estate-wide survey of
sha2exposure. Sites were identified statically(manifest and source inspection); they have not been compiled against 0.11.