---
id: "GHSA-pcc4-rvhr-2pr8"
kind: "vulnerability"
aliases: ["GHSA-pcc4-rvhr-2pr8"]
title: "Graphify Fortran extractor: incomplete cpp #include hardening allows arbitrary host-file read/exfiltration when indexing an untrusted repo"
severity: "medium"
cvss: null
epss: null
exploited: false
status: "open"
category: "package"
products: [{"name": "graphifyy", "fixed": ["0.9.70"], "affected": ">= 0.7.2, < 0.9.70", "ecosystem": "pypi"}]
ecosystems: ["pypi"]
tags: ["coding-agents", "cwe-22", "cwe-73", "graphifyy", "mcp", "package", "pypi", "python"]
not_affected_if: ["no-fortran-sources", "untrusted-repos-never-indexed", "version-0.9.70-or-later"]
published_at: "2026-10-19T16:51:36+00:00"
first_seen_at: "2026-10-23T19:11:32+00:00"
updated_at: "2026-10-23T20:15:37+00:00"
sources: [{"source": "ghsa", "url": "https://github.com/advisories/GHSA-pcc4-rvhr-2pr8", "first_seen_at": "2026-10-23T19:11:32+00:00"}]
---

# Graphify Fortran extractor: incomplete cpp #include hardening allows arbitrary host-file read/exfiltration when indexing an untrusted repo

Graphify's Fortran extractor runs the C preprocessor on .F/.F90 files, and the existing -nostdinc and -I /dev/null flags do not block absolute or ../ traversal #include paths. An attacker who gets a victim to index a malicious repository can read arbitrary local files on the host, and their contents end up in graph.json, GRAPH_REPORT.md, and the MCP tools.

_Summary, check and fix written by a model from the sources; labelled as such._

## Affected

- pypi:graphifyy affected >= 0.7.2, < 0.9.70 fixed 0.9.70

## How to check

Run pip show graphifyy and check whether the version is >= 0.7.2 and < 0.9.70.
Search the indexed repos for Fortran files: find . -iname '*.F' -o -iname '*.F90' -o -iname '*.F95' -o -iname '*.F03' -o -iname '*.F08'
Sources do not say whether graph.json or other artifacts already contain leaked content.

## Fix

Upgrade graphifyy to 0.9.70 or later.
Until then, do not run graphify on untrusted repositories that contain capital-F Fortran files, and treat any graph output from such runs as potentially containing host file contents.

## Timeline

- 2026-10-23T19:11:32+00:00 ghsa https://github.com/advisories/GHSA-pcc4-rvhr-2pr8

## From the sources

## 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.

---

_Free to read at a human pace. An API with keys and an MCP server are in preparation; early access: hello@infrastack.ai_
