{"record":{"id":"ddcbdac4cddc3b62","repo":"jdx/mise","slug":"invalid-blob-digest-expected-sha256-prefix-di","errorCode":null,"errorMessage":"invalid blob digest (expected sha256: prefix): {digest}","messagePattern":"invalid blob digest \\(expected sha256: prefix\\): (.+?)","errorType":"validation","errorClass":"eyre::Report","httpStatus":null,"severity":"error","filePath":"src/oci/layout.rs","lineNumber":140,"sourceCode":"        };\n        let path = self.root.join(\"index.json\");\n        file::write(&path, serde_json::to_vec_pretty(&index)?)?;\n        Ok(())\n    }\n\n    pub fn write_manifest(&self, manifest: &ImageManifest) -> Result<(String, u64)> {\n        let bytes = serde_json::to_vec(manifest)?;\n        self.write_blob(&bytes)\n    }\n}\n\n/// Validate that `digest` is a well-formed `sha256:<64 lowercase hex>` string.\n/// Guards against path traversal from a malicious registry returning something\n/// like `sha256:../../etc/passwd` as a layer digest — without this check, that\n/// would be used directly as a filesystem path component.\npub(crate) fn validate_sha256_digest(digest: &str) -> Result<()> {\n    let Some(hex) = digest.strip_prefix(\"sha256:\") else {\n        eyre::bail!(\"invalid blob digest (expected sha256: prefix): {digest}\");\n    };\n    if hex.len() != 64\n        || !hex\n            .chars()\n            .all(|c| c.is_ascii_digit() || ('a'..='f').contains(&c))\n    {\n        eyre::bail!(\"invalid blob digest (expected 64 lowercase hex chars): {digest}\");\n    }\n    Ok(())\n}\n\n#[cfg(test)]\nmod tests {\n    use super::*;\n\n    #[test]\n    fn rejects_path_traversal() {\n        assert!(validate_sha256_digest(\"sha256:../../etc/passwd\").is_err());","sourceCodeStart":122,"sourceCodeEnd":158,"githubUrl":"https://github.com/jdx/mise/blob/9dcfcaa0dc8747a2577d3270b69bb9d8313b2807/src/oci/layout.rs#L122-L158","documentation":"validate_sha256_digest is the gate before any digest is used as a filesystem path component (blobs/sha256/<hex>). It requires the literal 'sha256:' prefix; anything else — bare hex, 'sha512:...', digest objects stringified wrongly — is rejected here. This simultaneously blocks path traversal like 'sha256:../../etc/passwd'.","triggerScenarios":"A registry/manifest response supplying a bare 64-hex digest without the algorithm prefix, or a non-sha256 algorithm digest (sha512) reaching write_blob_with_digest; feeding a Descriptor's raw string that lost its prefix during serialization.","commonSituations":"Switching a manifest source to a registry that emits different digest formats; hand-written tooling that strips or reformats digests; consuming OCI artifacts not produced by mise.","solutions":["Normalize digests to 'sha256:<64 lowercase hex>' before passing them in (prepend the prefix when the algorithm is sha256).","Use digests exactly as they appear in the source manifest/config descriptors — do not reformat.","Reject non-sha256 content upstream; this code path only supports sha256."],"exampleFix":"// before\nlayout.write_blob_with_digest(hex_string, &bytes)?; // missing prefix\n\n// after\nlayout.write_blob_with_digest(&format!(\"sha256:{hex_string}\"), &bytes)?;","handlingStrategy":"type-guard","validationCode":null,"typeGuard":"fn is_sha256_digest(s: &str) -> bool {\n    let Some(hex) = s.strip_prefix(\"sha256:\") else { return false };\n    hex.len() == 64 && hex.bytes().all(|b| matches!(b, b'0'..=b'9' | b'a'..=b'f'))\n}","tryCatchPattern":null,"preventionTips":["Pass digests through verbatim from source descriptors; never reconstruct them by string surgery.","Reject non-sha256 algorithms at your boundary; this writer supports only sha256.","Add the one-line type guard wherever untrusted manifest data enters your pipeline."],"tags":["oci","digest","validation","path-traversal"],"backgroundTag":"oci-digest-validation","analyzedSha":"9dcfcaa0dc8747a2577d3270b69bb9d8313b2807","analyzedAt":"2026-08-17T14:28:50.624Z","schemaVersion":2},"datasetVersion":"2026-08-22T04:17:13.399Z"}