Loom / tools /sim_ssrf.py
deploy-space action
deploy 6158a00 (c)
b972454
Raw
History Blame Contribute Delete
4.43 kB
from __future__ import annotations
import asyncio
import os
import sys
from pathlib import Path
# Quota-free, NETWORK-FREE test of the SSRF guard: block internal/metadata addresses on
# the two outbound paths (web_fetch, repopack redirects), allow public ones.
# MOCK_MODE=0 python tools/sim_ssrf.py (web_fetch path needs MOCK off to exercise the guard)
os.environ.setdefault("LOOM_LOG", "0")
os.environ.pop("SSRF_ALLOW_PRIVATE", None) # ensure the guard is active
os.environ["MOCK_MODE"] = "0"
os.environ["WEB_TOOLS_MOCK"] = "0"
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
import ssrfguard # noqa: E402
from ssrfguard import BlockedAddress, assert_public_url # noqa: E402
FAILS: list[str] = []
def check(name, cond, detail=""):
print(f" [{'PASS' if cond else 'FAIL'}] {name}" + (f" ({detail})" if detail else ""))
if not cond: FAILS.append(name)
def blocked(url, resolver=None) -> bool:
try:
assert_public_url(url, resolver=resolver)
return False
except BlockedAddress:
return True
def main() -> int:
print("scenario: literal internal/metadata IPs are blocked (no DNS)")
for u in ["http://127.0.0.1/", "http://169.254.169.254/latest/meta-data/",
"http://10.0.0.1/", "http://192.168.1.1/", "http://172.16.0.5/",
"http://[::1]/", "http://0.0.0.0/", "https://[::ffff:127.0.0.1]/"]:
check(f"blocked {u}", blocked(u), "")
print("scenario: public literal IP is allowed")
check("8.8.8.8 allowed", not blocked("https://8.8.8.8/"))
check("1.1.1.1 allowed", not blocked("http://1.1.1.1/"))
print("scenario: hostname resolution is checked (injected resolver)")
evil = lambda host: ["169.254.169.254"] # noqa: E731 - host -> metadata IP
good = lambda host: ["140.82.121.3"] # noqa: E731 - host -> public IP
check("hostname resolving to metadata IP is blocked",
blocked("https://evil.example/", resolver=evil))
check("hostname resolving to public IP is allowed",
not blocked("https://codeload.github.com/x/y/tar.gz/main", resolver=good))
check("host resolving to NO addresses is blocked",
blocked("https://nx.example/", resolver=lambda h: []))
print("scenario: non-http schemes rejected")
check("file:// blocked", blocked("file:///etc/passwd"))
check("gopher:// blocked", blocked("gopher://127.0.0.1/"))
print("scenario: web_fetch returns the blocked error WITHOUT a network call")
import web_tools
# httpx client is never used because assert_public_url short-circuits first; pass a
# dummy whose .get would raise if it were ever called.
class _Boom:
async def get(self, *a, **k):
raise AssertionError("network call should NOT happen for a blocked host")
out = asyncio.run(web_tools.web_fetch("http://169.254.169.254/latest/meta-data/",
http_client=_Boom()))
check("web_fetch blocks metadata URL", "blocked address" in out, out)
print("scenario: repopack redirect handler blocks a redirect to an internal host")
import repopack
h = repopack._GuardedRedirectHandler()
raised = False
try:
h.redirect_request(req=None, fp=None, code=302, msg="", headers={},
newurl="http://169.254.169.254/")
except BlockedAddress:
raised = True
check("redirect to metadata is blocked", raised)
# a public redirect target is allowed by the guard (handler proceeds to super()).
ok_redirect = True
try:
assert_public_url("https://objects.githubusercontent.com/x", resolver=good)
except BlockedAddress:
ok_redirect = False
check("public redirect target passes the guard", ok_redirect)
print("scenario: SSRF_ALLOW_PRIVATE escape hatch")
os.environ["SSRF_ALLOW_PRIVATE"] = "1"
import importlib
importlib.reload(ssrfguard)
bypass = True
try:
ssrfguard.assert_public_url("http://127.0.0.1/")
except ssrfguard.BlockedAddress:
bypass = False
check("escape hatch allows localhost when set", bypass)
os.environ.pop("SSRF_ALLOW_PRIVATE", None)
importlib.reload(ssrfguard)
print()
if FAILS:
print(f"FAILED ({len(FAILS)}): {FAILS}"); return 1
print("ALL SSRF SCENARIOS PASSED (zero network calls)"); return 0
if __name__ == "__main__":
raise SystemExit(main())