harden ollama metadata egress and cpp path handling (F3, F5)
F3: OLLAMA_BASE_URL pointing at a link-local or cloud-metadata address (169.254.x, metadata.google.*, 0.0.0.0) now fails closed instead of only warning, since no real Ollama host lives there and it is a classic SSRF target. General LAN hosts still warn-and-allow. F5: the Fortran C-preprocessor call now passes an absolute path. cpp has no '--' end-of-options terminator, so an attacker-named corpus file like '-I/etc/x.F90' could otherwise be parsed as a cpp option; an absolute path always begins with '/' and cannot be. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
+4
-1
@@ -5248,8 +5248,11 @@ def _cpp_preprocess(path: Path) -> bytes:
|
||||
if not shutil.which("cpp"):
|
||||
return path.read_bytes()
|
||||
try:
|
||||
# Pass an absolute path so a corpus file named like "-I/etc/x.F90" cannot
|
||||
# be parsed by cpp as an option (cpp does not accept a "--" end-of-options
|
||||
# terminator). An absolute path always begins with "/".
|
||||
result = subprocess.run(
|
||||
["cpp", "-w", "-P", "-nostdinc", "-I", "/dev/null", str(path)],
|
||||
["cpp", "-w", "-P", "-nostdinc", "-I", "/dev/null", str(path.resolve())],
|
||||
capture_output=True,
|
||||
timeout=30,
|
||||
)
|
||||
|
||||
+13
-3
@@ -1251,11 +1251,13 @@ def estimate_cost(backend: str, input_tokens: int, output_tokens: int) -> float:
|
||||
|
||||
|
||||
def _validate_ollama_base_url(url: str) -> None:
|
||||
"""Warn (do not raise) if OLLAMA_BASE_URL looks unsafe.
|
||||
"""Warn if OLLAMA_BASE_URL looks unsafe; hard-block link-local/metadata (F3).
|
||||
|
||||
Sending an entire corpus to a non-loopback http:// endpoint silently leaks
|
||||
proprietary code; we surface a visible stderr warning instead of failing
|
||||
closed (some users genuinely run Ollama on a LAN host they trust).
|
||||
proprietary code, but some users genuinely run Ollama on a LAN host they
|
||||
trust, so a general non-loopback target only warns. A link-local or cloud
|
||||
metadata address (169.254.x, metadata.google.*) is never a legitimate Ollama
|
||||
host and is a classic SSRF target, so we fail closed with a ValueError there.
|
||||
"""
|
||||
try:
|
||||
from urllib.parse import urlparse
|
||||
@@ -1274,6 +1276,14 @@ def _validate_ollama_base_url(url: str) -> None:
|
||||
)
|
||||
return
|
||||
host = (parsed.hostname or "").lower()
|
||||
if (
|
||||
host.startswith("169.254.") # link-local, includes the 169.254.169.254 metadata IP
|
||||
or host in ("metadata.google.internal", "metadata.google.com", "0.0.0.0", "::", "[::]") # nosec B104 - blocklist, not a bind
|
||||
):
|
||||
raise ValueError(
|
||||
f"OLLAMA_BASE_URL points at a link-local/metadata address ({host!r}); refusing to "
|
||||
"send the corpus there. Set it to a real Ollama host."
|
||||
)
|
||||
is_loopback = host in ("localhost", "127.0.0.1", "::1") or host.startswith("127.")
|
||||
if not is_loopback:
|
||||
scheme_note = " (UNENCRYPTED)" if parsed.scheme == "http" else ""
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
"""The Fortran C-preprocessor path is hardened against argument injection (F5).
|
||||
|
||||
A corpus file is attacker-named; cpp does not accept a "--" end-of-options
|
||||
terminator, so _cpp_preprocess passes an absolute path which can never be parsed
|
||||
as a cpp option.
|
||||
"""
|
||||
from graphify import extract
|
||||
|
||||
|
||||
def test_cpp_preprocess_passes_absolute_path(tmp_path, monkeypatch):
|
||||
f = tmp_path / "weird.F90"
|
||||
f.write_text("program x\nend program x\n")
|
||||
|
||||
captured = {}
|
||||
|
||||
def fake_run(argv, **kwargs):
|
||||
captured["argv"] = argv
|
||||
|
||||
class _Result:
|
||||
returncode = 0
|
||||
stdout = b"preprocessed"
|
||||
|
||||
return _Result()
|
||||
|
||||
monkeypatch.setattr("shutil.which", lambda name: "/usr/bin/cpp")
|
||||
monkeypatch.setattr("subprocess.run", fake_run)
|
||||
|
||||
out = extract._cpp_preprocess(f)
|
||||
assert out == b"preprocessed"
|
||||
last_arg = captured["argv"][-1]
|
||||
assert last_arg.startswith("/"), f"path arg must be absolute, got {last_arg!r}"
|
||||
assert not last_arg.startswith("-"), "path arg must never look like an option"
|
||||
+23
-1
@@ -1,7 +1,29 @@
|
||||
"""Tests for the Ollama backend additions in graphify/llm.py."""
|
||||
from __future__ import annotations
|
||||
|
||||
from graphify.llm import detect_backend, BACKENDS
|
||||
import pytest
|
||||
|
||||
from graphify.llm import detect_backend, BACKENDS, _validate_ollama_base_url
|
||||
|
||||
|
||||
@pytest.mark.parametrize("url", [
|
||||
"http://169.254.169.254/v1",
|
||||
"http://169.254.1.5:11434/v1",
|
||||
"http://metadata.google.internal/v1",
|
||||
"http://0.0.0.0:11434/v1",
|
||||
])
|
||||
def test_ollama_blocks_link_local_and_metadata(url):
|
||||
"""Link-local / cloud-metadata Ollama targets fail closed (F3)."""
|
||||
with pytest.raises(ValueError):
|
||||
_validate_ollama_base_url(url)
|
||||
|
||||
|
||||
def test_ollama_loopback_and_lan_do_not_raise(capsys):
|
||||
"""Loopback is silent; a general LAN host warns but is allowed (F3)."""
|
||||
_validate_ollama_base_url("http://localhost:11434/v1")
|
||||
assert capsys.readouterr().err == ""
|
||||
_validate_ollama_base_url("http://192.168.1.50:11434/v1") # LAN: warn, not raise
|
||||
assert "non-loopback" in capsys.readouterr().err
|
||||
|
||||
|
||||
def test_ollama_in_backends():
|
||||
|
||||
Reference in New Issue
Block a user