GHSA-pcc4-rvhr-2pr8
Graphify Fortran extractor: incomplete cpp #include hardening allows arbitrary host-file read/exfiltration when indexing an untrusted repo
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:
- Absolute includes —
#include "/etc/passwd"is opened directly by cpp. - 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 topath.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.
Attack profile
How easy it is to exploit
- Attack vector
- Local
- Complexity
- Low
- Privileges needed
- None
- User interaction
- Passive
What an attacker can affect
- Confidentiality
- High
- Integrity
- None
- Availability
- None
What each source reports
GitHub Advisories Score used 6.8
Graphify Fortran extractor: incomplete cpp #include hardening allows arbitrary host-file read/exfiltration when indexing an untrusted repo
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:
- Absolute includes —
#include "/etc/passwd"is opened directly by cpp. - 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…
- Record
GHSA-pcc4-rvhr-2pr8- CVSS 4.0
- 6.8 Medium
CVSS:4.0/AV:L/AC:L/AT:N/PR:N/UI:P/VC:H/VI:N/VA:N/SC:N/SI:N/SA:N - Weakness
- CWE-22, CWE-73
- Updated
- 2026-11-02
OSV 6.8
Graphify Fortran extractor: incomplete cpp #include hardening allows arbitrary host-file read/exfiltration when indexing an untrusted repo
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:
- Absolute includes —
#include "/etc/passwd"is opened directly by cpp. - 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…
- Record
GHSA-pcc4-rvhr-2pr8- CVSS 4.0
- 6.8 Medium
CVSS:4.0/AV:L/AC:L/AT:N/PR:N/UI:P/VC:H/VI:N/VA:N/SC:N/SI:N/SA:N - Weakness
- CWE-22, CWE-73
- Updated
- 2026-11-02
Affected packages
| Package | Affected | Fixed |
|---|---|---|
graphifyyPyPI | 0.7.2 up to 0.9.70 | 0.9.70 |
graphifyyPyPI | 0.7.2 up to 0.9.70, plus listed versions 0.7.100.7.110.7.130.7.140.7.15 Show 45 moreShow less0.7.160.7.170.7.180.7.190.7.30.7.40.7.50.7.60.7.70.7.80.7.90.8.10.8.100.8.110.8.130.8.140.8.150.8.160.8.170.8.180.8.190.8.20.8.200.8.210.8.220.8.240.8.250.8.260.8.270.8.280.8.290.8.30.8.300.8.310.8.320.8.330.8.340.8.350.8.360.8.370.8.380.8.390.8.40.8.400.8.41 | 0.9.70 |
History
| Recorded | Changed | Sources |
|---|---|---|
| first indexed | GitHub Advisories, OSV |
Dates are when Every CVE recorded each change. Upstream dates may differ.
In short
GHSA-pcc4-rvhr-2pr8 is a medium vulnerability (CVSS 6.8) in graphifyy, published October 19, 2026. No source lists public exploit code. Fixed in graphifyy 0.9.70.
References
Left out here: 85 versions and the rest of 2 source descriptions. The JSON record has them.