GHSA-pcc4-rvhr-2pr8
GHSA-pcc4-rvhr-2pr8 · Graphify Fortran extractor: incomplete cpp #include hardening allows arbitrary host-file read/exfiltration when indexing an untrusted repo
This page contains facts from public vulnerability sources. Check the original advisory before updating.
## Summary The 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). This is the exact "corpus-side file-exfiltration vector" the F-007 comment says it is defending against; the mitigation is bypassable. ## Severity High. 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). ## Where `graphify/extractors/fortran.py`, `_cpp_preprocess()`: result = subprocess.run( ["cpp", "-w", "-P", "-nostdinc", "-I", "/dev/null", str(path.resolve())], capture_output=True, timeout=30, ) Reached 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`. ## Why the mitigation does not work `-nostdinc` only removes the standard *system* include directories, and `-I /dev/null` only sets the *additional* search path. Neither affects: 1. Absolute includes — `#include "/etc/passwd"` is opened directly by cpp. 2. 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. ## Proof of concept (verified) Reproduced against the shipped code with the exact command `_cpp_preprocess` builds (`cpp -w -P -nostdinc -I /dev/null <file>`), and against `extract_fortran()` directly. Secret on disk (`/tmp/secret.txt`): `TOP_SECRET_...` (a) Absolute include — `evil_abs.F90`: program evil #include "/absolute/path/to/secret.txt" end program evil (b) Relative traversal — `evil_rel.F90`: program evil #include "../secret.txt" end program evil Both produce preprocessed output containing the full secret file contents (`"SECRET IN OUTPUT: True"` for both). Surfacing 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: module m contains subroutine s use & #include "/path/to/token.secret" end subroutine s end module m `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. Fidelity 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. ## Suggested fix Mirror 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`: - Parse `#include "..."` / `#include <...>` directives. - 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). - Only allow includes that resolve to a path inside the corpus root. A 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. ## Detected by Aeon vuln-scanner. Semgrep flagged the Fortran subprocess path; manual review of the F-007 mitigation plus an empirical `cpp` repro confirmed the bypass.
CVSS 6.8 · Medium · Fix available
- Published
- Updated
Stack: python
Affected products and versions
graphifyy PyPI
- 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
- 0.8.41
- 0.8.42
- 0.8.43
- 0.8.44
- 0.8.45
- 0.8.46
- 0.8.47
- 0.8.48
- 0.8.49
- 0.8.5
- 0.8.50
- 0.8.51
- 0.8.6
- 0.8.7
- 0.8.8
- 0.8.9
- 0.9.0
- 0.9.1
- 0.9.10
- 0.9.11
- 0.9.12
- 0.9.13
- 0.9.14
- 0.9.15
- 0.9.16
- 0.9.17
- 0.9.18
- 0.9.19
- 0.9.2
- 0.9.20
- 0.9.21
- 0.9.22
- 0.9.23
- 0.9.24
- 0.9.25
- 0.9.26
- 0.9.27
- 0.9.28
- 0.9.29
- 0.9.3
- 0.9.30
- 0.9.31
- 0.9.32
- 0.9.33
- 0.9.34
- 0.9.35
- 0.9.36
- 0.9.37
- 0.9.38
- 0.9.39
- 0.9.4
- 0.9.40
- 0.9.41
- 0.9.42
- 0.9.43
- 0.9.44
- 0.9.45
- 0.9.46
- 0.9.47
- 0.9.48
- 0.9.49
- 0.9.5
- 0.9.50
- 0.9.51
- 0.9.52
- 0.9.53
- 0.9.54
- 0.9.55
- 0.9.56
- 0.9.57
- 0.9.58
- 0.9.59
- 0.9.6
- 0.9.60
- 0.9.61
- 0.9.62
- 0.9.63
- 0.9.64
- 0.9.65
- 0.9.66
- 0.9.67
- 0.9.68
- 0.9.69
- 0.9.7
- 0.9.8
- 0.9.9
- Affected ranges
- ECOSYSTEM: ≥ 0.7.2, < 0.9.70
Update to 0.9.70
Original sources
- https://github.com/Graphify-Labs/graphify/security/advisories/GHSA-pcc4-rvhr-2pr8
- https://github.com/Graphify-Labs/graphify/commit/e1d9deed6a437f2c85458ef3c42519159f322b7b
- https://github.com/Graphify-Labs/graphify
- https://github.com/Graphify-Labs/graphify/releases/tag/v0.9.70
- https://osv.dev/vulnerability/GHSA-pcc4-rvhr-2pr8