{"record":{"id":"6c0663e9481190ac","repo":"headroomlabs-ai/headroom","slug":"fixture-declares-transform-but-lives-under","errorCode":null,"errorMessage":"fixture {} declares transform={} but lives under {}","messagePattern":"fixture (.+?) declares transform=(.+?) but lives under (.+?)","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"warning","filePath":"crates/headroom-parity/src/lib.rs","lineNumber":100,"sourceCode":"\n/// Load every `*.json` fixture under `dir/<transform>/`.\npub fn load_fixtures_for(dir: &Path, transform: &str) -> Result<Vec<(PathBuf, Fixture)>> {\n    let root = dir.join(transform);\n    if !root.exists() {\n        return Ok(Vec::new());\n    }\n    let mut out = Vec::new();\n    for entry in fs::read_dir(&root).with_context(|| format!(\"reading {}\", root.display()))? {\n        let entry = entry?;\n        let path = entry.path();\n        if path.extension().and_then(|s| s.to_str()) != Some(\"json\") {\n            continue;\n        }\n        let bytes = fs::read(&path).with_context(|| format!(\"reading {}\", path.display()))?;\n        let fixture: Fixture = serde_json::from_slice(&bytes)\n            .with_context(|| format!(\"parsing fixture {}\", path.display()))?;\n        if fixture.transform != transform {\n            bail!(\n                \"fixture {} declares transform={} but lives under {}\",\n                path.display(),\n                fixture.transform,\n                transform\n            );\n        }\n        out.push((path, fixture));\n    }\n    Ok(out)\n}\n\n/// Aggregate report of one comparator run.\n#[derive(Debug, Default)]\npub struct Report {\n    pub matched: usize,\n    pub diffed: Vec<(PathBuf, String, String)>,\n    pub skipped: Vec<(PathBuf, String)>,\n}","sourceCodeStart":82,"sourceCodeEnd":118,"githubUrl":"https://github.com/headroomlabs-ai/headroom/blob/322425c43bffde1ed0b64fecf3cf5951565dd82b/crates/headroom-parity/src/lib.rs#L82-L118","documentation":"The second 404 gate in require_loopback (loopback_guard.py:216): the TCP peer is loopback but the Host header does not name a loopback host. This is the DNS-rebinding defense — an attacker's DNS name (e.g. attacker.com) resolves to 127.0.0.1 so the peer IP passes, but the browser-sent Host header betrays the non-local origin, and the request is 404'd.","triggerScenarios":"Browsing to http://your-hostname:8080/debug/... where the hostname resolves to 127.0.0.1 but is not 'localhost'/an ip-literal; tools that set a custom Host header (e.g. curl -H 'Host: mybox.local'); a rebinding attack where a public domain flips to 127.0.0.1.","commonSituations":"Using a machine's hostname or a LAN DNS name that happens to resolve to loopback; curl with explicit Host overrides; legitimate dashboards accessed via a named vhost.","solutions":["Use http://127.0.0.1:PORT or http://localhost:PORT directly","Remove Host header overrides from your curl command","If a name is required, use a name that resolves to a loopback literal and ensure the Host header itself is localhost or a 127.x address"],"exampleFix":"# before\ncurl -H 'Host: mybox.local' http://127.0.0.1:8080/debug/tasks\n\n# after\ncurl http://127.0.0.1:8080/debug/tasks","handlingStrategy":"validation","validationCode":"from urllib.parse import urlparse\nhost = urlparse(url).hostname\nassert host in (\"127.0.0.1\", \"localhost\") or host.startswith(\"127.\"), f\"Host {host} will be 404'd by the loopback guard\"","typeGuard":null,"tryCatchPattern":null,"preventionTips":["Always call local admin endpoints via 127.0.0.1, not a hostname","Remove Host overrides from curl commands targeting debug routes"],"tags":["security","dns-rebinding","host-header","http-404"],"backgroundTag":null,"analyzedSha":"322425c43bffde1ed0b64fecf3cf5951565dd82b","analyzedAt":"2026-08-15T01:03:05.481Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}