{"schema":"threatline.record/2","record":{"id":"GHSA-pcc4-rvhr-2pr8","aliases":["GHSA-pcc4-rvhr-2pr8"],"summary":"Graphify Fortran extractor: incomplete cpp #include hardening allows arbitrary host-file read/exfiltration when indexing an untrusted repo","details":"## Summary\n\nThe Fortran extractor's C-preprocessor step (`graphify/extractors/fortran.py`, `_cpp_preprocess`) applies an incomplete `#include` hardening. The `-nostdinc -I /dev/null` flags it relies on (comment \"Security (F-007)\") do **not** stop a capital-`F` Fortran source file from including **absolute paths** (`#include \"/etc/passwd\"`) or from **path-traversing out of the corpus** (`#include \"../../../../home/victim/.ssh/id_rsa\"`). `cpp` always searches the directory of the file being preprocessed for quoted includes and always honours absolute include paths, regardless of `-nostdinc`/`-I`. As a result, indexing an untrusted repository that contains a `.F`/`.F90`/`.F95`/`.F03`/`.F08` file reads arbitrary local files off the host and inlines their contents into the knowledge graph, which is then shipped to the LLM/agent and written to the on-disk, shareable output (`graph.json`, `GRAPH_REPORT.md`, the Obsidian wiki, HTML/SVG exports).\n\nThis is the exact \"corpus-side file-exfiltration vector\" the F-007 comment says it is defending against; the mitigation is bypassable.\n\n## Severity\n\nHigh. Arbitrary local-file read on the host, triggered by graphify's core action (indexing an untrusted folder — \"turn any folder of code ... into a queryable knowledge graph\"), defeating a documented security control. It runs on the default, offline, no-API-key AST path (`graphify update .` / `graphify extract`) — no credentials, network, or LLM call required. CWE-73 (external control of file name/path) / CWE-22 (path traversal) / CWE-200 (information exposure).\n\n## Where\n\n`graphify/extractors/fortran.py`, `_cpp_preprocess()`:\n\n    result = subprocess.run(\n        [\"cpp\", \"-w\", \"-P\", \"-nostdinc\", \"-I\", \"/dev/null\", str(path.resolve())],\n        capture_output=True, timeout=30,\n    )\n\nReached from `extract_fortran()` (same file) for any suffix in `_FORTRAN_CPP_EXTS = {\".F\", \".F90\", \".F95\", \".F03\", \".F08\"}`, which are collected by default (`detect.CODE_EXTENSIONS`) and dispatched by `extract._DISPATCH`.\n\n## Why the mitigation does not work\n\n`-nostdinc` only removes the standard *system* include directories, and `-I /dev/null` only sets the *additional* search path. Neither affects:\n\n1. Absolute includes — `#include \"/etc/passwd\"` is opened directly by cpp.\n2. Quoted includes relative to the current file — cpp always searches the directory containing the file being preprocessed for `#include \"...\"`, so `#include \"../../../../etc/shadow\"` traverses out of the corpus.\n\n## Proof of concept (verified)\n\nReproduced against the shipped code with the exact command `_cpp_preprocess` builds (`cpp -w -P -nostdinc -I /dev/null <file>`), and against `extract_fortran()` directly.\n\nSecret on disk (`/tmp/secret.txt`): `TOP_SECRET_...`\n\n(a) Absolute include — `evil_abs.F90`:\n\n    program evil\n    #include \"/absolute/path/to/secret.txt\"\n    end program evil\n\n(b) Relative traversal — `evil_rel.F90`:\n\n    program evil\n    #include \"../secret.txt\"\n    end program evil\n\nBoth produce preprocessed output containing the full secret file contents (`\"SECRET IN OUTPUT: True\"` for both).\n\nSurfacing into the graph — `leak.F90` places the include in an identifier position via a free-form continuation so the leaked value becomes a node/edge:\n\n    module m\n    contains\n    subroutine s\n    use &\n    #include \"/path/to/token.secret\"\n    end subroutine s\n    end module m\n\n`extract_fortran()` on this file emits a node whose label is the entire single-line contents of `token.secret` and an `imports` edge to it. That node is written to `graph.json` / `GRAPH_REPORT.md` / the wiki and returned by the MCP `query_graph` / `get_node` / `get_neighbors` tools — i.e. exfiltrated into the agent's context and into persisted, shareable artifacts.\n\nFidelity note: Fortran is case-insensitive, so captured identifiers are lower-cased. Case-insensitive secrets (file paths, hostnames, usernames, hex tokens, connection-string fragments, and the mere existence/partial content of a readable file) leak cleanly; high-entropy case-sensitive blobs (base64/JWT) are partially corrupted by the lower-casing but still confirm readability and structure. Single-token-per-line secret files (API-key files, single-line credential/token files) leak that token in full (lower-cased). Requires `cpp`/gcc on PATH (near-ubiquitous on the dev/CI hosts graphify targets); it cleanly falls back to raw bytes when absent.\n\n## Suggested fix\n\nMirror the containment approach already used elsewhere in the codebase (`_project_xml_is_safe`, `security.validate_graph_path`): before invoking cpp, scan the source for `#include` directives and **refuse or neutralise any include whose target is absolute or resolves outside the corpus/repo root**. Concretely, in `_cpp_preprocess`:\n\n- Parse `#include \"...\"` / `#include <...>` directives.\n- Resolve each target against the file's directory; if it is absolute, or its `.resolve()` escapes the scan root, strip that line (or fall back to `path.read_bytes()` with no preprocessing).\n- Only allow includes that resolve to a path inside the corpus root.\n\nA blunter but safe alternative is to strip all `#include` lines before preprocessing (macros still expand; cross-file includes lose fidelity but no host files can be read). Running cpp inside a filesystem sandbox (bwrap/unshare with only the corpus mounted) would also close it but is heavier.\n\n## Detected by\n\nAeon vuln-scanner. Semgrep flagged the Fortran subprocess path; manual review of the F-007 mitigation plus an empirical `cpp` repro confirmed the bypass.","categories":["Libraries & packages"],"product":"graphifyy","vendor":"","ecosystem":"pip","affected_ranges":[{"source":"GitHub Advisory Database","source_url":"https://github.com/advisories/GHSA-pcc4-rvhr-2pr8","source_modified_at":"2026-10-19T16:52:26Z","fetched_at":"2026-10-23T18:42:43.628194Z","package":{"ecosystem":"pip","name":"graphifyy"},"range":">= 0.7.2, < 0.9.70"},{"source":"OSV","source_url":"https://osv.dev/vulnerability/GHSA-pcc4-rvhr-2pr8","source_modified_at":"2026-10-19T17:00:04.142006586Z","fetched_at":"2026-10-23T18:42:43.628194Z","package":{"name":"graphifyy","ecosystem":"PyPI","purl":"pkg:pypi/graphifyy"},"ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"0.7.2"},{"fixed":"0.9.70"}]}],"versions":["0.7.10","0.7.11","0.7.13","0.7.14","0.7.15","0.7.16","0.7.17","0.7.18","0.7.19","0.7.2","0.7.3","0.7.4","0.7.5","0.7.6","0.7.7","0.7.8","0.7.9","0.8.1","0.8.10","0.8.11","0.8.13","0.8.14","0.8.15","0.8.16","0.8.17","0.8.18","0.8.19","0.8.2","0.8.20","0.8.21","0.8.22","0.8.24","0.8.25","0.8.26","0.8.27","0.8.28","0.8.29","0.8.3","0.8.30","0.8.31","0.8.32","0.8.33","0.8.34","0.8.35","0.8.36","0.8.37","0.8.38","0.8.39","0.8.4","0.8.40"]}],"fixed_versions":[{"source":"GitHub Advisory Database","source_url":"https://github.com/advisories/GHSA-pcc4-rvhr-2pr8","source_modified_at":"2026-10-19T16:52:26Z","fetched_at":"2026-10-23T18:42:43.628194Z","package":{"ecosystem":"pip","name":"graphifyy"},"version":"0.9.70"},{"source":"OSV","source_url":"https://osv.dev/vulnerability/GHSA-pcc4-rvhr-2pr8","source_modified_at":"2026-10-19T17:00:04.142006586Z","fetched_at":"2026-10-23T18:42:43.628194Z","package":{"name":"graphifyy","ecosystem":"PyPI","purl":"pkg:pypi/graphifyy"},"version":"0.9.70"}],"cvss":6.8,"severity":"Medium","kev":{"listed":false},"epss":{"probability":null,"percentile":null,"score_date":null},"published_at":"2026-10-19T16:51:36Z","modified_at":"2026-10-19T17:00:04.142006586Z","provenance":[{"source":"GitHub Advisory Database","url":"https://github.com/advisories/GHSA-pcc4-rvhr-2pr8","fetched_at":"2026-10-23T18:42:43.628194Z","source_modified_at":"2026-10-19T16:52:26Z"},{"source":"OSV","url":"https://osv.dev/vulnerability/GHSA-pcc4-rvhr-2pr8","fetched_at":"2026-10-23T18:42:43.628194Z","source_modified_at":"2026-10-19T17:00:04.142006586Z"}],"sources":["GitHub Advisory Database","OSV"],"references":[{"url":"https://github.com/Graphify-Labs/graphify/security/advisories/GHSA-pcc4-rvhr-2pr8","source":"GitHub Advisory Database"},{"url":"https://github.com/Graphify-Labs/graphify/commit/e1d9deed6a437f2c85458ef3c42519159f322b7b","source":"GitHub Advisory Database"},{"url":"https://github.com/Graphify-Labs/graphify/releases/tag/v0.9.70","source":"GitHub Advisory Database"},{"url":"https://github.com/advisories/GHSA-pcc4-rvhr-2pr8","source":"GitHub Advisory Database"}],"locations":[],"motoko_assessment":{"status":"requires_architecture_review","automatic_remediation":false},"classification":{"method":"product-first software-layer rules","rules_version":"2.1","matched_on":"ecosystem","confidence":"heuristic","reviewed":false},"change_timestamp":"2026-10-19T17:00:04.142006586Z","content_hash":"585e76addcb851b6398f27ef2026964e9978265f4ec9e783af033eb656874bf9"}}
