fix(security): harden Python service boundaries

This commit is contained in:
Alexandre Teixeira
2026-10-06 03:16:54 +01:00
parent da3800b662
commit 3d91ad82cb
32 changed files with 1911 additions and 248 deletions
@@ -123,3 +123,37 @@ async def test_update_event_can_add_reminder_after_creation():
assert note is not None, "update_event should create the reminder note"
finally:
db.close()
async def test_reminder_and_duration_parsers_stay_linear_on_digit_and_space_floods():
"""CodeQL py/polynomial-redos: `(\\d+)\\s*unit` rescanned a digit run from
every offset and `alarm\\s*:?\\s*\\d+` split one whitespace run two ways.
Tool arguments come from model output, so keep them O(n)."""
import time
from src.tool_implementations import do_manage_calendar
owner = "tester-" + uuid.uuid4().hex[:6]
started = time.perf_counter()
res = await do_manage_calendar(json.dumps({
"action": "create_event",
"summary": "Flood",
"dtstart": "2030-04-01T10:00:00",
"reminder_minutes": "0" * 40_000 + "x",
"duration": "0" * 40_000 + "x",
}), owner=owner)
assert time.perf_counter() - started < 2.0
assert res.get("exit_code") == 0, res
assert "reminder set" not in res.get("response", ""), res
# A reminder is set, so the "is the description only a reminder?" check runs.
started = time.perf_counter()
res = await do_manage_calendar(json.dumps({
"action": "create_event",
"summary": "Flood 2",
"dtstart": "2030-04-02T10:00:00",
"description": "alarm" + "\t" * 40_000,
"reminder_minutes": 5,
}), owner=owner)
assert time.perf_counter() - started < 2.0
assert res.get("exit_code") == 0, res
assert "reminder 5 min before" in res.get("response", ""), res
@@ -0,0 +1,522 @@
"""Final closure tests for the residual Python CodeQL lane (PR #6503).
1. MCP attachment extraction: no traversal, per owner/account directories.
2. Mail DNS rebinding: one resolution, connect only to the checked address,
TLS SNI/verification still against the requested hostname.
3. /accounts/test runs its blocking mail I/O off the event loop.
4. /search/web works under the production CSP and payloads cannot execute.
5. Outbound mail address policy matrix.
"""
import asyncio
import json
import os
import re
import shutil
import socket
import subprocess
import threading
import time
import urllib.parse
from email.mime.application import MIMEApplication
from email.mime.multipart import MIMEMultipart
from pathlib import Path
from types import SimpleNamespace
import pytest
# -- 1. MCP attachment extraction -------------------------------------------
def _message_with_attachment(body: bytes) -> bytes:
msg = MIMEMultipart()
part = MIMEApplication(body, Name="invoice.pdf")
part["Content-Disposition"] = 'attachment; filename="invoice.pdf"'
msg.attach(part)
return msg.as_bytes()
@pytest.fixture
def mcp_mail(tmp_path, monkeypatch):
import mcp_servers.email_server as es
root = tmp_path / "mail-attachments"
root.mkdir()
monkeypatch.setattr(es, "MAIL_ATTACHMENTS_DIR", str(root))
monkeypatch.setattr(es, "_fixture_attachment_source", lambda *a, **k: None)
monkeypatch.setattr(es, "_load_config", lambda account=None: {"account_id": account})
box = {"raw": _message_with_attachment(b"%PDF one")}
class _Conn:
def select(self, *a, **k):
return ("OK", [b"1"])
def uid(self, *a):
return ("OK", [(b"42 (BODY[])", box["raw"])])
def logout(self):
pass
monkeypatch.setattr(es, "_imap_connect", lambda account=None: _Conn())
return es, root.resolve(), box
@pytest.mark.parametrize("folder,uid", [
("/tmp/odysseus-escape", "42"), # absolute mailbox name replaced the root
("../../escape", "42"), # relative traversal
("INBOX/../../../escape", "42"), # hierarchy delimiter + traversal
("..", ".."),
("INBOX", "../../42"),
("INBOX/Receipts", "42"), # legitimate hierarchical folder
])
def test_mcp_download_attachment_stays_in_one_root_segment(mcp_mail, folder, uid):
es, root, _box = mcp_mail
result = es._download_attachment(uid, 0, folder)
path = Path(result["path"]).resolve()
assert path.parent.parent == root, path
assert path.read_bytes() == b"%PDF one"
def test_mcp_download_attachment_isolates_owners_and_accounts(mcp_mail):
es, _root, box = mcp_mail
paths = {}
for owner, account, body in (
("alice", "acct-a", b"%PDF alice"),
("bob", "acct-a", b"%PDF bob"),
("alice", "acct-b", b"%PDF alice second mailbox"),
):
token = es._CURRENT_OWNER.set(owner)
try:
box["raw"] = _message_with_attachment(body)
paths[(owner, account)] = (es._download_attachment("42", 0, "INBOX", account=account)["path"], body)
finally:
es._CURRENT_OWNER.reset(token)
assert len({p for p, _ in paths.values()}) == 3
for path, body in paths.values():
assert Path(path).read_bytes() == body
def test_attachment_scope_dir_rejects_symlinked_scope(tmp_path):
from src.mail_attachment_paths import attachment_scope_dir
root = tmp_path / "root"
root.mkdir()
target = attachment_scope_dir(root, "INBOX", "1", owner="o", account_id="a")
outside = tmp_path / "outside"
outside.mkdir()
target.symlink_to(outside, target_is_directory=True)
with pytest.raises(ValueError):
attachment_scope_dir(root, "INBOX", "1", owner="o", account_id="a")
def test_attachment_scope_dir_normalises_uid_type(tmp_path):
from src.mail_attachment_paths import attachment_scope_dir
assert attachment_scope_dir(tmp_path, "INBOX", 42, owner="o", account_id=None) == \
attachment_scope_dir(tmp_path, "INBOX", "42", owner="o", account_id="")
# -- 2. DNS rebinding: resolve once, connect to the checked address ------------
class _LineServer:
"""Tiny loopback server speaking just enough IMAP or SMTP for a handshake."""
def __init__(self, protocol):
self.protocol = protocol
self.sock = socket.socket()
self.sock.bind(("127.0.0.1", 0))
self.sock.listen(4)
self.port = self.sock.getsockname()[1]
self.thread = threading.Thread(target=self._serve, daemon=True)
self.thread.start()
def _serve(self):
try:
conn, _ = self.sock.accept()
except OSError:
return
with conn, conn.makefile("rwb") as io:
io.write(b"* OK ready\r\n" if self.protocol == "imap" else b"220 fake ESMTP\r\n")
io.flush()
for raw in io:
line = raw.decode().strip()
if self.protocol == "imap":
tag, _, command = line.partition(" ")
if command.upper().startswith("CAPABILITY"):
io.write(f"* CAPABILITY IMAP4rev1\r\n{tag} OK done\r\n".encode())
else:
io.write(f"* BYE\r\n{tag} OK bye\r\n".encode())
io.flush()
return
elif line.upper().startswith(("EHLO", "HELO")):
io.write(b"250 fake\r\n")
else:
io.write(b"221 bye\r\n")
io.flush()
return
io.flush()
def close(self):
self.sock.close()
@pytest.fixture
def rebinding_dns(monkeypatch):
"""`rebind.test` answers loopback once, then the metadata address."""
real = socket.getaddrinfo
lookups = []
def _resolve(host, port, *args, **kwargs):
if host == "rebind.test":
lookups.append(host)
ip = "127.0.0.1" if len(lookups) == 1 else "169.254.169.254"
return [(socket.AF_INET, socket.SOCK_STREAM, 6, "", (ip, port))]
return real(host, port, *args, **kwargs)
monkeypatch.setattr(socket, "getaddrinfo", _resolve)
return lookups
class _RecordingTLS:
"""Stands in for an SSLContext: records the SNI name, keeps the socket plain."""
def __init__(self):
self.server_hostname = None
def wrap_socket(self, sock, server_hostname=None, **_kwargs):
self.server_hostname = server_hostname
return sock
@pytest.mark.parametrize("tls", [False, True])
def test_imap_connects_to_the_checked_address_only(rebinding_dns, tls):
from routes.email.email_helpers import _PolicyIMAP4, _PolicyIMAP4_SSL
server = _LineServer("imap")
try:
if tls:
context = _RecordingTLS()
conn = _PolicyIMAP4_SSL("rebind.test", server.port, block_private=False, timeout=5, ssl_context=context)
assert context.server_hostname == "rebind.test"
else:
conn = _PolicyIMAP4("rebind.test", server.port, block_private=False, timeout=5)
assert conn.sock.getpeername()[0] == "127.0.0.1"
conn.logout()
finally:
server.close()
assert rebinding_dns == ["rebind.test"] # a second lookup would have answered 169.254.169.254
@pytest.mark.parametrize("tls", [False, True])
def test_smtp_connects_to_the_checked_address_only(rebinding_dns, tls):
from routes.email.email_helpers import _PolicySMTP, _PolicySMTP_SSL
server = _LineServer("smtp")
try:
if tls:
context = _RecordingTLS()
smtp = _PolicySMTP_SSL("rebind.test", server.port, block_private=False, timeout=5, context=context)
assert context.server_hostname == "rebind.test"
else:
smtp = _PolicySMTP("rebind.test", server.port, block_private=False, timeout=5)
assert smtp.sock.getpeername()[0] == "127.0.0.1"
assert smtp.ehlo()[0] == 250
smtp.quit()
finally:
server.close()
assert rebinding_dns == ["rebind.test"]
def test_any_denied_answer_in_a_mixed_resolution_blocks_the_connection():
from src.url_safety import OutboundAddressBlocked, connect_outbound_tcp
def _mixed(host, port, *args):
return [
(socket.AF_INET, socket.SOCK_STREAM, 6, "", ("93.184.216.34", port)),
(socket.AF_INET, socket.SOCK_STREAM, 6, "", ("169.254.169.254", port)),
]
with pytest.raises(OutboundAddressBlocked):
connect_outbound_tcp("mixed.test", 993, block_private=False, resolver=_mixed)
# -- 5. Address/principal policy matrix ----------------------------------------
_ALWAYS_DENIED = [
"169.254.169.254", "fe80::1", "0.0.0.0", "::", "224.0.0.1", "240.0.0.1",
"::ffff:169.254.169.254", "64:ff9b::a9fe:a9fe", "64:ff9b::a00:5",
"::1", # IPv6 loopback sits in ::/8, which the existing policy treats as reserved
]
_PRIVATE = ["127.0.0.1", "10.0.0.5", "172.16.0.1", "192.168.1.10", "100.64.0.1", "fd00::1"]
_PUBLIC = ["93.184.216.34", "2606:2800:220:1::1"]
@pytest.fixture
def fake_sockets(monkeypatch):
import src.url_safety as url_safety
connected = []
class _Sock:
def __init__(self, *args):
pass
def settimeout(self, timeout):
pass
def bind(self, address):
pass
def connect(self, sockaddr):
connected.append(sockaddr[0])
def close(self):
pass
monkeypatch.setattr(url_safety.socket, "socket", _Sock)
return connected
def _answer(ip):
family = socket.AF_INET6 if ":" in ip else socket.AF_INET
return lambda host, port, *args: [(family, socket.SOCK_STREAM, 6, "", (ip, port))]
@pytest.mark.parametrize("block_private", [False, True])
@pytest.mark.parametrize("ip", _ALWAYS_DENIED)
def test_metadata_and_special_ranges_are_always_denied(fake_sockets, ip, block_private):
from src.url_safety import OutboundAddressBlocked, connect_outbound_tcp
with pytest.raises(OutboundAddressBlocked):
connect_outbound_tcp("h", 993, block_private=block_private, resolver=_answer(ip))
assert fake_sockets == []
@pytest.mark.parametrize("ip", _PRIVATE)
def test_private_ranges_follow_the_principal_policy(fake_sockets, ip):
from src.url_safety import OutboundAddressBlocked, connect_outbound_tcp
connect_outbound_tcp("h", 993, block_private=False, resolver=_answer(ip))
assert fake_sockets == [ip]
with pytest.raises(OutboundAddressBlocked):
connect_outbound_tcp("h", 993, block_private=True, resolver=_answer(ip))
@pytest.mark.parametrize("ip", _PUBLIC)
def test_public_mail_servers_are_always_allowed(fake_sockets, ip):
from src.url_safety import connect_outbound_tcp
connect_outbound_tcp("h", 993, block_private=True, resolver=_answer(ip))
assert fake_sockets == [ip]
@pytest.mark.parametrize("trusted,env,owner,blocked", [
(False, {}, "bob", True), # multi-user, non-admin: denied
(False, {}, "", True), # multi-user, no principal: denied
(True, {}, "admin", False), # admin (or single-user mode)
(False, {"EMAIL_ALLOW_PRIVATE_IPS": "true"}, "bob", False), # operator opt-in for LAN mail
(True, {"EMAIL_BLOCK_PRIVATE_IPS": "true"}, "admin", True), # operator lockdown wins
(False, {"EMAIL_ALLOW_PRIVATE_IPS": "true", "EMAIL_BLOCK_PRIVATE_IPS": "true"}, "bob", True),
])
def test_private_mail_destination_principal_matrix(monkeypatch, trusted, env, owner, blocked):
import src.tool_security as tool_security
from routes.email.email_helpers import _mail_private_blocked
for key in ("EMAIL_ALLOW_PRIVATE_IPS", "EMAIL_BLOCK_PRIVATE_IPS"):
monkeypatch.delenv(key, raising=False)
for key, value in env.items():
monkeypatch.setenv(key, value)
monkeypatch.setattr(tool_security, "owner_is_admin_or_single_user", lambda o: trusted)
assert _mail_private_blocked(owner) is blocked
def test_single_user_mode_allows_lan_mail_servers(monkeypatch):
from routes.email.email_helpers import _mail_private_blocked
for key in ("EMAIL_ALLOW_PRIVATE_IPS", "EMAIL_BLOCK_PRIVATE_IPS"):
monkeypatch.delenv(key, raising=False)
monkeypatch.setenv("AUTH_ENABLED", "false")
assert _mail_private_blocked("") is False
def test_non_admin_cannot_probe_loopback_through_the_connection_test(monkeypatch):
import routes.email_routes as email_routes
import src.tool_security as tool_security
for key in ("EMAIL_ALLOW_PRIVATE_IPS", "EMAIL_BLOCK_PRIVATE_IPS"):
monkeypatch.delenv(key, raising=False)
monkeypatch.setattr(tool_security, "owner_is_admin_or_single_user", lambda o: False)
endpoint = _accounts_test_endpoint(email_routes)
class _Request:
async def json(self):
return {"imap_host": "127.0.0.1", "imap_port": 6379, "imap_user": "u", "imap_password": "p",
"smtp_host": "10.0.0.5", "smtp_port": 25}
result = asyncio.run(endpoint(req=_Request(), owner="bob"))
assert result["imap"]["error"] == "IMAP server address is not allowed by this server's network policy"
assert result["smtp"]["error"] == "SMTP server address is not allowed by this server's network policy"
# -- 3. /accounts/test blocking I/O stays off the event loop -----------------------
def _accounts_test_endpoint(email_routes):
router = email_routes.setup_email_routes()
return next(
r.endpoint for r in router.routes
if r.path == "/api/email/accounts/test" and "POST" in getattr(r, "methods", set())
)
def test_connection_test_does_not_block_the_event_loop(monkeypatch):
import routes.email_routes as email_routes
def _slow_open(host, port, **kwargs):
time.sleep(1.0) # a mail server that never answers within the timeout
raise socket.timeout("timed out")
monkeypatch.setattr(email_routes, "_open_imap_connection", _slow_open)
endpoint = _accounts_test_endpoint(email_routes)
class _Request:
async def json(self):
return {"imap_host": "imap.example.com", "imap_port": 993, "imap_user": "u", "imap_password": "p"}
async def _main():
gaps = []
done = asyncio.Event()
async def _heartbeat():
last = time.perf_counter()
while not done.is_set():
await asyncio.sleep(0.02)
now = time.perf_counter()
gaps.append(now - last)
last = now
beat = asyncio.create_task(_heartbeat())
started = time.perf_counter()
results = await asyncio.gather(*(endpoint(req=_Request(), owner="alice") for _ in range(3)))
elapsed = time.perf_counter() - started
done.set()
await beat
return results, max(gaps), elapsed
results, worst_gap, elapsed = asyncio.run(_main())
assert worst_gap < 0.5, f"event loop stalled for {worst_gap:.2f}s"
assert elapsed < 2.5 # three 1s probes overlapped instead of serialising on the loop
for result in results:
assert result == {"ok": False, "imap": {"ok": False, "error": "IMAP connection timed out"}, "smtp": None}
def test_connection_test_keeps_its_authentication_dependency():
import routes.email_routes as email_routes
from routes.email.email_routes import require_user
router = email_routes.setup_email_routes()
route = next(
r for r in router.routes
if r.path == "/api/email/accounts/test" and "POST" in getattr(r, "methods", set())
)
assert any(dep.call is require_user for dep in route.dependant.dependencies)
# -- 4. /search/web under the production CSP ---------------------------------------
_PAYLOAD = (
"</script><script>document.title='PWNED'</script>"
"<img src=x onerror=\"document.body.dataset.pwned='1'\">"
)
_SOURCES = [
{"title": "Example result", "url": "https://example.com/a", "snippet": "first"},
{"title": "Script link", "url": "javascript:document.body.dataset.pwned='2'", "snippet": "second"},
]
def _csp_app():
from fastapi import FastAPI
from fastapi.responses import HTMLResponse
from core.middleware import SecurityHeadersMiddleware
from routes.search.search_routes import setup_search_routes
app = FastAPI()
app.add_middleware(SecurityHeadersMiddleware)
@app.post("/api/search")
async def _stub_search():
return {"sources": _SOURCES}
@app.get("/csp-control")
async def _control():
return HTMLResponse("<html><body><script>document.body.dataset.ran='1'</script></body></html>")
app.include_router(setup_search_routes(SimpleNamespace()))
return app
@pytest.mark.parametrize("q", ["weather in lisbon", _PAYLOAD])
def test_search_page_script_carries_the_response_csp_nonce(q):
from fastapi.testclient import TestClient
response = TestClient(_csp_app()).get("/search/web", params={"q": q})
csp = response.headers["content-security-policy"]
nonce = re.search(r"'nonce-([^']+)'", csp).group(1)
assert "unsafe-inline" not in csp.split("script-src", 1)[1].split(";", 1)[0]
scripts = re.findall(r"<script\b[^>]*>", response.text, re.I)
assert scripts == [f'<script nonce="{nonce}">']
markup = response.text.split("<script", 1)[0]
assert "<img" not in markup.lower() # the payload is only ever present HTML-escaped
def _chrome():
for name in ("google-chrome", "google-chrome-stable", "chromium", "chromium-browser"):
path = shutil.which(name)
if path:
return path
return None
@pytest.fixture
def live_csp_server():
uvicorn = pytest.importorskip("uvicorn")
with socket.socket() as probe:
probe.bind(("127.0.0.1", 0))
port = probe.getsockname()[1]
server = uvicorn.Server(uvicorn.Config(_csp_app(), host="127.0.0.1", port=port, log_level="error"))
thread = threading.Thread(target=server.run, daemon=True)
thread.start()
deadline = time.time() + 10
while not server.started and time.time() < deadline:
time.sleep(0.05)
yield f"http://127.0.0.1:{port}"
server.should_exit = True
thread.join(timeout=10)
def _render(chrome, url, profile):
out = subprocess.run(
[chrome, "--headless=new", "--disable-gpu", "--no-sandbox", "--no-first-run",
f"--user-data-dir={profile}", "--virtual-time-budget=5000", "--dump-dom", url],
capture_output=True, text=True, timeout=90,
)
return out.stdout
@pytest.mark.skipif(_chrome() is None, reason="needs a headless Chrome/Chromium")
def test_search_page_runs_under_production_csp_and_payload_does_not(live_csp_server, tmp_path):
chrome = _chrome()
# Control: the production CSP really blocks nonce-less inline script.
control = _render(chrome, f"{live_csp_server}/csp-control", tmp_path / "p0")
assert "<body>" in control and "data-ran" not in control
normal = _render(chrome, f"{live_csp_server}/search/web?q=weather", tmp_path / "p1")
assert "2 results" in normal # the page's script executed
assert 'href="https://example.com/a"' in normal
assert "javascript:" not in normal.split('id="results"', 1)[1] # non-http result URL not linked
hostile = _render(
chrome, f"{live_csp_server}/search/web?q={urllib.parse.quote(_PAYLOAD)}", tmp_path / "p2",
)
assert "2 results" in hostile # search still ran with that query
assert hostile.split("<title>", 1)[1].split("</title>", 1)[0] != "PWNED" # escaped text, not run
assert "data-pwned" not in hostile
@@ -0,0 +1,260 @@
"""Follow-up security fixes for the residual Python CodeQL lane (PR #6503).
A. upload-reference scanners stay linear and match the regexes they replaced.
B. /api/hwfit/* (SSH host probing, host path probing) is admin-only.
C. IMAP/SMTP connections honour the outbound address policy, and the account
connection test never echoes a non-mail service's bytes.
D. attachment extraction directories are per owner/account, not per folder/UID.
"""
import asyncio
import contextlib
import imaplib
import re
import smtplib
import socket
import ssl
import time
from email.mime.application import MIMEApplication
from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText
from types import SimpleNamespace
import pytest
# -- A. upload reference scanners ---------------------------------------------
_HEX = "a" * 32
_PDF_REF = re.compile(
r"<!--\s*pdf(?:_form)?_source\b[^>]*\bupload_id=[\"']([0-9a-fA-F]{32}(?:\.[A-Za-z0-9]+)?)[\"'][^>]*-->",
re.IGNORECASE,
)
_ATTACHMENT_REF = re.compile(
r"\[Attachment:[^\]\r\n]*\|\s*id=([0-9a-fA-F]{32}(?:\.[A-Za-z0-9]+)?)(?:\s*\||\s*\])",
re.IGNORECASE,
)
@pytest.mark.parametrize("text", [
f"<!-- pdf_source upload_id='{_HEX}' -->",
f"<!--PDF_FORM_SOURCE x upload_id=\"{_HEX}.pdf\" y-->",
f"<!-- pdf_source upload_id='{_HEX}' upload_id='{'b' * 32}' -->",
f"<!-- pdf_source upload_id='{_HEX}' ->",
f"<!-- pdf_sourceupload_id='{_HEX}' -->",
f"<!--pdf_source <!--pdf_source upload_id='{_HEX}'-->",
"<!-- pdf_source no id -->",
])
def test_pdf_source_ids_match_reference(text):
from src.upload_handler import _pdf_source_upload_ids
assert _pdf_source_upload_ids(text) == _PDF_REF.findall(text)
@pytest.mark.parametrize("text", [
f"[Attachment: report.pdf | id={_HEX} | 12 KB]",
f"[attachment: a | b | id={_HEX}.txt]",
f"[Attachment: a | id={_HEX} | id={'b' * 32}]",
f"[Attachment: x |\nid={_HEX}]",
f"[Attachment: [Attachment: n | id={_HEX}]",
f"[Attachment: n | id={_HEX}x]",
"[Attachment: no id here]",
])
def test_attachment_reference_ids_match_reference(text):
from src.upload_handler import _attachment_reference_ids
assert _attachment_reference_ids(text) == _ATTACHMENT_REF.findall(text)
@pytest.mark.parametrize("flood", [
"[attachment:" * 20_000, # CodeQL's reported shape (#615)
"[Attachment: a |" * 15_000,
"<!-- pdf_source upload_id=" * 9_000, # same sink, other pattern
"<!--pdf_source " + f"upload_id='{_HEX}' " * 5_000 + "-->",
])
def test_internal_upload_id_extraction_is_linear(flood):
from src.upload_handler import extract_internal_upload_ids
started = time.perf_counter()
extract_internal_upload_ids(flood)
assert time.perf_counter() - started < 2.0
# -- B. hwfit authorization -----------------------------------------------------
def _hwfit_client(monkeypatch, *, auth_disabled=False):
from fastapi import FastAPI, Request
from fastapi.testclient import TestClient
import core.middleware as middleware
import services.hwfit.hardware as hardware
from routes.hwfit_routes import setup_hwfit_routes
probes = []
monkeypatch.setattr(middleware, "auth_disabled", lambda: auth_disabled)
monkeypatch.setattr(
hardware, "detect_system",
lambda **kwargs: probes.append(kwargs) or {"backend": "cpu_x86"},
)
app = FastAPI()
app.state.auth_manager = SimpleNamespace(is_configured=True, is_admin=lambda user: user == "root")
@app.middleware("http")
async def _user(request: Request, call_next):
request.state.current_user = request.headers.get("x-test-user")
return await call_next(request)
app.include_router(setup_hwfit_routes())
return TestClient(app), probes
@pytest.mark.parametrize("path", [
"/api/hwfit/system?host=10.0.0.5&ssh_port=2222",
"/api/hwfit/models?host=169.254.169.254",
"/api/hwfit/profiles?model_path=/etc&host=",
"/api/hwfit/image-models?host=internal-db",
])
def test_hwfit_rejects_non_admin_before_any_probe(monkeypatch, path):
client, probes = _hwfit_client(monkeypatch)
response = client.get(path, headers={"x-test-user": "alice"})
assert response.status_code == 403
assert probes == []
def test_hwfit_allows_admin_and_single_user(monkeypatch):
client, probes = _hwfit_client(monkeypatch)
assert client.get("/api/hwfit/system", headers={"x-test-user": "root"}).status_code == 200
client, probes = _hwfit_client(monkeypatch, auth_disabled=True)
assert client.get("/api/hwfit/system", headers={"x-test-user": "anyone"}).status_code == 200
assert probes
# -- C. mail server address policy and test-connection errors -------------------
def test_imap_and_smtp_helpers_refuse_metadata_hosts_before_any_socket(monkeypatch):
"""Real helper paths (no stdlib patching): the policy rejects a literal
metadata address before a socket is even created. Policy matrix and DNS
rebinding coverage live in test_codeql_python_residual_closure.py."""
import routes.email.email_helpers as helpers
monkeypatch.delenv("EMAIL_BLOCK_PRIVATE_IPS", raising=False)
with pytest.raises(helpers.MailServerAddressBlocked):
helpers._open_imap_connection("169.254.169.254", 993, starttls=False, owner="alice")
with pytest.raises(helpers.MailServerAddressBlocked):
helpers._send_smtp_message(
{"smtp_host": "169.254.169.254", "smtp_port": 587, "owner": "alice"}, "a@b", ["c@d"], "x",
)
@pytest.mark.parametrize("error,expected,leak", [
(imaplib.IMAP4.abort("unexpected response: b'SSH-2.0-OpenSSH_9.6 internal-db'"),
"IMAP server did not respond like an IMAP server", "OpenSSH"),
(smtplib.SMTPConnectError(554, b"-ERR redis 7.2 internal-cache"),
"SMTP server did not respond like an SMTP server", "redis"),
(smtplib.SMTPServerDisconnected("Connection unexpectedly closed: HTTP/1.1 400"),
"SMTP server did not respond like an SMTP server", "HTTP/1.1"),
(ConnectionRefusedError(111, "Connection refused 10.0.0.5:6379"), "IMAP connection refused", "6379"),
(socket.timeout("timed out"), "IMAP connection timed out", "10.0"),
(ssl.SSLError(1, "[SSL: WRONG_VERSION_NUMBER] banner: nginx"),
"IMAP TLS handshake failed; check the port and security setting", "nginx"),
])
def test_connection_test_errors_never_echo_peer_bytes(error, expected, leak):
from routes.email.email_helpers import _mail_connection_test_error
protocol = expected.split()[0]
message = _mail_connection_test_error(protocol, "10.0.0.5", error)
assert message == expected
assert leak not in message
def test_connection_test_keeps_mail_server_auth_responses():
from routes.email.email_helpers import _mail_connection_test_error
assert _mail_connection_test_error(
"IMAP", "imap.example.com", imaplib.IMAP4.error("[AUTHENTICATIONFAILED] Invalid credentials"),
) == "[AUTHENTICATIONFAILED] Invalid credentials"
microsoft = _mail_connection_test_error(
"SMTP", "smtp.office365.com",
smtplib.SMTPAuthenticationError(535, b"5.7.139 Authentication unsuccessful"),
)
assert "Microsoft no longer accepts" in microsoft
def test_account_connection_test_route_applies_policy_to_both_protocols(monkeypatch):
import routes.email_routes as email_routes
monkeypatch.delenv("EMAIL_BLOCK_PRIVATE_IPS", raising=False)
router = email_routes.setup_email_routes()
endpoint = next(
r.endpoint for r in router.routes
if r.path == "/api/email/accounts/test" and "POST" in getattr(r, "methods", set())
)
class _Request:
async def json(self):
return {
"imap_host": "169.254.169.254", "imap_port": 993, "imap_user": "u", "imap_password": "p",
"smtp_host": "169.254.169.254", "smtp_port": 587,
}
result = asyncio.run(endpoint(req=_Request(), owner="alice"))
policy = "server address is not allowed by this server's network policy"
assert result["ok"] is False
assert result["imap"]["error"] == f"IMAP {policy}"
assert result["smtp"]["error"] == f"SMTP {policy}"
# -- D. per-owner/account attachment extraction -------------------------------
def _message_with_attachment(body: bytes) -> bytes:
msg = MIMEMultipart()
msg.attach(MIMEText("see attached", "plain"))
part = MIMEApplication(body, Name="invoice.pdf")
part["Content-Disposition"] = 'attachment; filename="invoice.pdf"'
msg.attach(part)
return msg.as_bytes()
def test_attachment_extract_dir_separates_owners_and_accounts(tmp_path, monkeypatch):
import routes.email.email_helpers as helpers
monkeypatch.setattr(helpers, "ATTACHMENTS_DIR", tmp_path)
alice = helpers.attachment_extract_dir("INBOX", "42", owner="alice", account_id="a1")
assert alice != helpers.attachment_extract_dir("INBOX", "42", owner="bob", account_id="a1")
assert alice != helpers.attachment_extract_dir("INBOX", "42", owner="alice", account_id="a2")
assert alice != helpers.attachment_extract_dir("INBOX/42", "", owner="alice", account_id="a1")
assert alice.parent == tmp_path.resolve()
def test_two_users_same_folder_uid_cannot_overwrite_each_others_attachment(tmp_path, monkeypatch):
"""Two principals, same INBOX UID 42, same attachment filename.
Before the fix both extractions landed in ATTACHMENTS_DIR/INBOX_42/, so
bob's request rewrote the file whose path alice's agent was about to read.
"""
import routes.email.email_helpers as helpers
import routes.email_routes as email_routes
monkeypatch.setattr(helpers, "ATTACHMENTS_DIR", tmp_path)
mailboxes = {
"alice": _message_with_attachment(b"%PDF alice private invoice"),
"bob": _message_with_attachment(b"%PDF bob controlled content"),
}
@contextlib.contextmanager
def _fake_imap(account_id=None, owner=""):
yield SimpleNamespace(owner=owner, select=lambda *a, **k: ("OK", [b"1"]))
monkeypatch.setattr(email_routes, "_imap", _fake_imap)
monkeypatch.setattr(
email_routes, "_imap_uid_fetch",
lambda conn, uid, query: ("OK", [(b"42 (RFC822)", mailboxes[conn.owner])]),
)
router = email_routes.setup_email_routes()
endpoint = next(r.endpoint for r in router.routes if r.path == "/api/email/attachment-path/{uid}/{index}")
alice = asyncio.run(endpoint(uid="42", index=0, folder="INBOX", account_id=None, owner="alice"))
bob = asyncio.run(endpoint(uid="42", index=0, folder="INBOX", account_id=None, owner="bob"))
assert alice["filename"] == bob["filename"] == "invoice.pdf"
assert alice["path"] != bob["path"]
with open(alice["path"], "rb") as handle:
assert handle.read() == b"%PDF alice private invoice"
@@ -0,0 +1,164 @@
"""Adversarial tests for the residual Python CodeQL fixes (PR #6503 lane).
Covers py/reflective-xss (search page), py/path-injection (context_info host
path oracle) and py/stack-trace-exposure (readiness, stream errors, task
drafting).
"""
import asyncio
import json
import re
from types import SimpleNamespace
import pytest
# -- py/reflective-xss: GET /search/web?q= ----------------------------------
_XSS_QUERIES = [
'</script><script src="https://cdn.jsdelivr.net/gh/evil/x@1/p.js"></script>',
"</SCRIPT><img src=x onerror=alert(1)>",
"<!--<script>",
"a & b < c > d 
 \"quoted\" 'single'",
]
def _search_page(q):
from routes.search.search_routes import setup_search_routes
router = setup_search_routes(SimpleNamespace())
endpoint = next(r.endpoint for r in router.routes if r.path == "/search/web")
request = SimpleNamespace(state=SimpleNamespace(csp_nonce="n0nce"))
return asyncio.run(endpoint(request=request, q=q)).body.decode("utf-8")
@pytest.mark.parametrize("q", _XSS_QUERIES)
def test_search_page_query_cannot_break_out_of_inline_script(q):
page = _search_page(q)
script = page.split('<script nonce="n0nce">', 1)[1]
# The page's own closing tag is the only one; nothing from q survives raw.
assert script.lower().count("</script") == 1
assert "<!--" not in script
literal = re.search(r"const initialQuery = (.*?);\n", script).group(1)
assert "<" not in literal and ">" not in literal and "&" not in literal
# The JS string value is unchanged for the search itself.
assert json.loads(literal) == q.strip()
# -- py/path-injection: /session/{id}/context_info?cwd= ----------------------
def _context_info(monkeypatch, *, admin, cwd):
import routes.session_routes as routes
from fastapi import APIRouter
monkeypatch.setattr(routes, "_verify_session_owner", lambda *args: None)
monkeypatch.setattr("src.tool_security.blocked_tools_for_owner", lambda owner: set())
monkeypatch.setattr("src.tool_security.owner_is_admin_or_single_user", lambda owner: admin)
monkeypatch.setattr(routes, "router", APIRouter(prefix="/api"))
session = SimpleNamespace(endpoint_url="", model="", cwd="")
router = routes.setup_session_routes(SimpleNamespace(get_session=lambda sid: session), {})
endpoint = next(
r.endpoint for r in router.routes if r.path == "/api/session/{session_id}/context_info"
)
request = SimpleNamespace(state=SimpleNamespace(
api_token=False, api_token_owner=None, api_token_scopes=[], current_user="someone",
))
return asyncio.run(endpoint(request, "session-1", cwd=cwd))
def test_context_info_hides_host_paths_from_non_admin(monkeypatch, tmp_path):
(tmp_path / "AGENTS.md").write_text("x", encoding="utf-8")
result = _context_info(monkeypatch, admin=False, cwd=str(tmp_path))
assert result["agents_md"] == []
assert result["workspace"]["exists_in_backend"] is False
def test_context_info_still_reports_host_paths_for_admin(monkeypatch, tmp_path):
(tmp_path / "AGENTS.md").write_text("x", encoding="utf-8")
result = _context_info(monkeypatch, admin=True, cwd=str(tmp_path))
assert {"path": str(tmp_path / "AGENTS.md"), "source": "workspace"} in result["agents_md"]
assert result["workspace"]["exists_in_backend"] is True
# -- py/stack-trace-exposure ---------------------------------------------------
_SECRET = "postgresql://odysseus:hunter2@10.0.0.5/prod /srv/odysseus/data"
def test_readiness_does_not_return_raw_database_error(monkeypatch):
import core.database as database
from src.readiness import check_readiness
class _Engine:
def connect(self):
raise RuntimeError(_SECRET)
monkeypatch.setattr(database, "engine", _Engine())
result = check_readiness()
db_check = result["checks"]["database"]
assert db_check == {"ok": False, "error_type": "RuntimeError"}
assert "hunter2" not in json.dumps(result)
assert result["ready"] is False
def test_readiness_does_not_return_raw_data_dir_error(monkeypatch, tmp_path):
import core.constants as constants
from src.readiness import check_readiness
blocker = tmp_path / "not-a-dir"
blocker.write_text("file, not a directory", encoding="utf-8")
monkeypatch.setattr(constants, "DATA_DIR", str(blocker / "data"))
result = check_readiness()
data_check = result["checks"]["data_dir"]
assert data_check["ok"] is False
assert set(data_check) == {"ok", "error_type"}
assert str(tmp_path) not in json.dumps(result)
def test_stream_failure_message_is_generic():
from src.llm_core import _stream_failure_message
assert _stream_failure_message(RuntimeError(_SECRET)) == "Model stream failed (RuntimeError)"
def test_catch_all_stream_error_events_do_not_embed_exception_text():
from pathlib import Path
source = (Path(__file__).resolve().parents[1] / "src/llm_core.py").read_text(encoding="utf-8")
assert '{"error": str(e), "status": 502' not in source
def _parse_task(monkeypatch, exc):
from fastapi import HTTPException # noqa: F401 (raised by callers below)
from unittest.mock import MagicMock
import routes.task.task_routes as task_routes
import src.endpoint_resolver as resolver
import src.llm_core as llm_core
async def _boom(*args, **kwargs):
raise exc
monkeypatch.setattr(resolver, "resolve_endpoint", lambda *a, **k: ("http://llm", "m", {}))
monkeypatch.setattr(llm_core, "llm_call_async", _boom)
router = task_routes.setup_task_routes(MagicMock())
endpoint = next(r.endpoint for r in router.routes if r.path == "/api/tasks/parse")
class _Request:
state = SimpleNamespace(current_user="alice")
async def json(self):
return {"description": "every day at 9 summarize the news"}
return asyncio.run(endpoint(_Request()))
def test_parse_task_hides_unexpected_exception_text(monkeypatch):
result = _parse_task(monkeypatch, RuntimeError(_SECRET))
assert result == {"success": False, "message": "Could not draft a task (RuntimeError)"}
def test_parse_task_keeps_curated_upstream_http_error(monkeypatch):
from fastapi import HTTPException
result = _parse_task(monkeypatch, HTTPException(401, "Upstream rejected the API key"))
assert result == {"success": False, "message": "Upstream rejected the API key"}
+6 -6
View File
@@ -26,7 +26,7 @@ class _FakeSock:
class _FakeIMAP:
calls = []
def __init__(self, host, port, timeout=None):
def __init__(self, host, port, timeout=None, **_policy):
self.host = host
self.port = port
self.timeout = timeout
@@ -60,8 +60,8 @@ def test_open_imap_connection_uses_shared_timeout_for_implicit_ssl(monkeypatch):
import routes.email_helpers as helpers
_FakeIMAP.calls = []
monkeypatch.setattr(helpers.imaplib, "IMAP4", _FakeIMAP)
monkeypatch.setattr(helpers.imaplib, "IMAP4_SSL", _FakeIMAPSSL)
monkeypatch.setattr(helpers, "_PolicyIMAP4", _FakeIMAP)
monkeypatch.setattr(helpers, "_PolicyIMAP4_SSL", _FakeIMAPSSL)
conn = _open_imap_connection("imap.one.com", 993, starttls=False)
@@ -75,8 +75,8 @@ def test_open_imap_connection_supports_starttls(monkeypatch):
import routes.email_helpers as helpers
_FakeIMAP.calls = []
monkeypatch.setattr(helpers.imaplib, "IMAP4", _FakeIMAP)
monkeypatch.setattr(helpers.imaplib, "IMAP4_SSL", _FakeIMAPSSL)
monkeypatch.setattr(helpers, "_PolicyIMAP4", _FakeIMAP)
monkeypatch.setattr(helpers, "_PolicyIMAP4_SSL", _FakeIMAPSSL)
_open_imap_connection("imap.local", 143, starttls=True)
@@ -99,7 +99,7 @@ async def test_account_config_uses_shared_imap_timeout(monkeypatch):
def logout(self):
captured["logout"] = True
def fake_open(host, port, *, starttls, timeout):
def fake_open(host, port, *, starttls, timeout, **_policy):
captured["open"] = (host, port, starttls, timeout)
return _Conn()
+1 -1
View File
@@ -64,7 +64,7 @@ def test_imap_connect_with_host_still_connects(monkeypatch):
def login(self, user, password):
opened["login"] = (user, password)
def _fake_open(host, port, *, starttls, timeout):
def _fake_open(host, port, *, starttls, timeout, **_policy):
opened["host"] = host
return _FakeConn()
+9 -9
View File
@@ -12,7 +12,7 @@ from routes.email_helpers import _send_smtp_message
class _FakeSMTP:
calls = []
def __init__(self, host, port, timeout=None):
def __init__(self, host, port, timeout=None, **_policy):
self.host = host
self.port = port
self.timeout = timeout
@@ -54,8 +54,8 @@ def test_send_smtp_message_supports_plain_smtp(monkeypatch):
import routes.email_helpers as helpers
_FakeSMTP.calls = []
monkeypatch.setattr(helpers.smtplib, "SMTP", _FakeSMTP)
monkeypatch.setattr(helpers.smtplib, "SMTP_SSL", _FakeSMTPSSL)
monkeypatch.setattr(helpers, "_PolicySMTP", _FakeSMTP)
monkeypatch.setattr(helpers, "_PolicySMTP_SSL", _FakeSMTPSSL)
_send_smtp_message(_cfg("none"), "from@example.com", ["to@example.com"], "hello")
@@ -68,8 +68,8 @@ def test_send_smtp_message_supports_explicit_starttls(monkeypatch):
import routes.email_helpers as helpers
_FakeSMTP.calls = []
monkeypatch.setattr(helpers.smtplib, "SMTP", _FakeSMTP)
monkeypatch.setattr(helpers.smtplib, "SMTP_SSL", _FakeSMTPSSL)
monkeypatch.setattr(helpers, "_PolicySMTP", _FakeSMTP)
monkeypatch.setattr(helpers, "_PolicySMTP_SSL", _FakeSMTPSSL)
_send_smtp_message(_cfg("starttls", port=2525), "from@example.com", ["to@example.com"], "hello")
@@ -82,8 +82,8 @@ def test_send_smtp_message_defaults_587_to_starttls(monkeypatch):
import routes.email_helpers as helpers
_FakeSMTP.calls = []
monkeypatch.setattr(helpers.smtplib, "SMTP", _FakeSMTP)
monkeypatch.setattr(helpers.smtplib, "SMTP_SSL", _FakeSMTPSSL)
monkeypatch.setattr(helpers, "_PolicySMTP", _FakeSMTP)
monkeypatch.setattr(helpers, "_PolicySMTP_SSL", _FakeSMTPSSL)
cfg = _cfg("", port=587)
_send_smtp_message(cfg, "from@example.com", ["to@example.com"], "hello")
@@ -96,8 +96,8 @@ def test_send_smtp_message_uses_ssl_when_configured(monkeypatch):
import routes.email_helpers as helpers
_FakeSMTP.calls = []
monkeypatch.setattr(helpers.smtplib, "SMTP", _FakeSMTP)
monkeypatch.setattr(helpers.smtplib, "SMTP_SSL", _FakeSMTPSSL)
monkeypatch.setattr(helpers, "_PolicySMTP", _FakeSMTP)
monkeypatch.setattr(helpers, "_PolicySMTP_SSL", _FakeSMTPSSL)
_send_smtp_message(_cfg("ssl", port=465), "from@example.com", ["to@example.com"], "hello")
+12 -12
View File
@@ -89,8 +89,8 @@ async def test_test_connection_oauth_account_uses_xoauth2_for_imap_and_smtp():
with mock.patch("core.database.SessionLocal", Factory), \
mock.patch("routes.email_routes._open_imap_connection", return_value=mock_imap_conn), \
mock.patch("routes.email_routes.smtplib.SMTP", return_value=mock_smtp_conn), \
mock.patch("routes.email_routes.smtplib.SMTP_SSL", return_value=mock_smtp_conn), \
mock.patch("routes.email_routes._PolicySMTP", return_value=mock_smtp_conn), \
mock.patch("routes.email_routes._PolicySMTP_SSL", return_value=mock_smtp_conn), \
mock.patch("routes.email_routes._get_valid_google_token", return_value="ya29.live") as token_getter:
result = await test_conn(req=_FakeReq(), owner="alice")
@@ -141,8 +141,8 @@ async def test_test_connection_password_account_still_uses_login():
with mock.patch("core.database.SessionLocal", Factory), \
mock.patch("routes.email_routes._open_imap_connection", return_value=mock_imap_conn), \
mock.patch("routes.email_routes.smtplib.SMTP", return_value=mock_smtp_conn), \
mock.patch("routes.email_routes.smtplib.SMTP_SSL", return_value=mock_smtp_conn):
mock.patch("routes.email_routes._PolicySMTP", return_value=mock_smtp_conn), \
mock.patch("routes.email_routes._PolicySMTP_SSL", return_value=mock_smtp_conn):
result = await test_conn(req=_FakeReq(), owner="alice")
assert result["ok"] is True
@@ -187,8 +187,8 @@ async def test_test_connection_rejects_non_google_hosts_before_oauth_auth():
with mock.patch("core.database.SessionLocal", Factory), \
mock.patch("routes.email_routes._open_imap_connection") as open_imap, \
mock.patch("routes.email_routes.smtplib.SMTP") as open_smtp, \
mock.patch("routes.email_routes.smtplib.SMTP_SSL") as open_smtp_ssl, \
mock.patch("routes.email_routes._PolicySMTP") as open_smtp, \
mock.patch("routes.email_routes._PolicySMTP_SSL") as open_smtp_ssl, \
mock.patch("routes.email_routes._get_valid_google_token") as token_getter:
result = await test_conn(req=_FakeReq(), owner="alice")
@@ -238,8 +238,8 @@ async def test_test_connection_rejects_insecure_oauth_transports_before_auth():
with mock.patch("core.database.SessionLocal", Factory), \
mock.patch("routes.email_routes._open_imap_connection") as open_imap, \
mock.patch("routes.email_routes.smtplib.SMTP") as open_smtp, \
mock.patch("routes.email_routes.smtplib.SMTP_SSL") as open_smtp_ssl, \
mock.patch("routes.email_routes._PolicySMTP") as open_smtp, \
mock.patch("routes.email_routes._PolicySMTP_SSL") as open_smtp_ssl, \
mock.patch("routes.email_routes._get_valid_google_token") as token_getter:
result = await test_conn(req=_FakeReq(), owner="alice")
@@ -336,10 +336,10 @@ async def test_test_connection_verifies_imap_tls_before_loading_oauth_token(
"routes.email_routes.ssl.create_default_context",
return_value=context,
), mock.patch(
"routes.email_helpers.imaplib.IMAP4",
"routes.email_helpers._PolicyIMAP4",
return_value=starttls_conn,
) as imap_cls, mock.patch(
"routes.email_helpers.imaplib.IMAP4_SSL",
"routes.email_helpers._PolicyIMAP4_SSL",
side_effect=ssl.SSLCertVerificationError("untrusted certificate"),
) as imap_ssl_cls, mock.patch(
"routes.email_routes._get_valid_google_token"
@@ -412,10 +412,10 @@ async def test_test_connection_verifies_smtp_tls_before_loading_oauth_token(
"routes.email_routes.ssl.create_default_context",
return_value=context,
), mock.patch(
"routes.email_routes.smtplib.SMTP",
"routes.email_routes._PolicySMTP",
return_value=starttls_smtp,
) as smtp_cls, mock.patch(
"routes.email_routes.smtplib.SMTP_SSL",
"routes.email_routes._PolicySMTP_SSL",
side_effect=ssl.SSLCertVerificationError("untrusted certificate"),
) as smtp_ssl_cls, mock.patch(
"routes.email_routes._get_valid_google_token"
+1 -1
View File
@@ -281,7 +281,7 @@ def test_open_imap_connection_shuts_down_on_starttls_failure(monkeypatch):
))
conn.starttls = MagicMock(side_effect=RuntimeError("STARTTLS rejected"))
monkeypatch.setattr(helpers.imaplib, "IMAP4", lambda *a, **kw: conn)
monkeypatch.setattr(helpers, "_PolicyIMAP4", lambda *a, **kw: conn)
raised = False
try:
@@ -0,0 +1,60 @@
"""manage_notes reminder-time sniffing must stay linear (CodeQL py/polynomial-redos).
`\\d{1,2}(?::\\d{2})?\\s*(?:am|pm)?\\s+today` let `\\s*` and `\\s+` split one
whitespace run every possible way, so a digit followed by a long whitespace
run in model-supplied note text was O(n^2). The rewrite groups the optional
meridiem with its own whitespace: same language, one way to match.
"""
import json
import sys
import time
import uuid
import pytest
from tests.helpers.import_state import clear_fake_database_modules
from tests.helpers.sqlite_db import make_temp_sqlite
clear_fake_database_modules()
import core.database as cdb
_TS, _ENGINE, _TMPDB = make_temp_sqlite(cdb.Base.metadata)
@pytest.fixture(autouse=True)
def _bind_temp_db(monkeypatch):
monkeypatch.setitem(sys.modules, "core.database", cdb)
parent = sys.modules.get("core")
if parent is not None:
monkeypatch.setattr(parent, "database", cdb, raising=False)
monkeypatch.setattr(cdb, "SessionLocal", _TS)
yield
async def _add(content):
from src.tool_implementations import do_manage_notes
return await do_manage_notes(
json.dumps({"action": "add", "title": "remind me", "content": content}),
owner="tester-" + uuid.uuid4().hex[:6],
)
async def test_reminder_time_sniffing_whitespace_flood_is_linear():
started = time.perf_counter()
res = await _add("remind 1" + "\t" * 40_000 + "x")
assert time.perf_counter() - started < 2.0
assert res.get("exit_code", 0) == 0, res
async def test_reminder_time_sniffing_still_finds_time_first_phrases():
res = await _add("remind me to call mom, 9 pm tomorrow")
assert res.get("exit_code", 0) == 0, res
db = _TS()
try:
note = db.query(cdb.Note).filter(cdb.Note.id == res["note_id"]).first()
assert note is not None and "T21:00:00" in (note.due_date or ""), note and note.due_date
finally:
db.close()
+183
View File
@@ -0,0 +1,183 @@
"""Regression tests for the residual py/polynomial-redos fixes (PR #6503 lane).
Each rewritten matcher is pinned two ways:
* equivalence with the regex it replaced on ordinary inputs, and
* the CodeQL-reported adversarial shape completing within a loose budget
(the replaced regexes took seconds to tens of seconds on these sizes).
"""
import re
import time
from pathlib import Path
import pytest
from src.text_helpers import strip_closed_think_blocks
_BUDGET_S = 2.0
_ROOT = Path(__file__).resolve().parents[1]
def _fast(fn, *args):
started = time.perf_counter()
result = fn(*args)
elapsed = time.perf_counter() - started
assert elapsed < _BUDGET_S, f"{getattr(fn, '__name__', fn)} took {elapsed:.2f}s"
return result
# -- closed <think> blocks (skills_routes x4, memory_extractor.audit_memories) --
_THINK_REF = re.compile(r"<think(?:ing)?>[\s\S]*?</think(?:ing)?>", re.I)
@pytest.mark.parametrize("text", [
"",
"plain",
"<think>x</think>{\"ok\": true}",
"a<thinking>b</thinking>c<think>d</think>e",
"<think>a<think>nested</think>rest</think>",
"orphan</think> then <think>unclosed",
"<THINK>case</Thinking>kept",
"<think>\nmulti\nline\n</think>\n{}",
])
def test_strip_closed_think_blocks_matches_reference(text):
assert strip_closed_think_blocks(text) == _THINK_REF.sub("", text)
def test_strip_closed_think_blocks_unclosed_opener_flood_is_linear():
flood = "<think>" * 40_000
assert _fast(strip_closed_think_blocks, flood) == flood
def test_lazy_think_regex_is_gone_from_llm_review_parsers():
for rel in ("routes/skills_routes.py", "services/memory/memory_extractor.py"):
source = (_ROOT / rel).read_text(encoding="utf-8")
assert r"<think(?:ing)?>[\s\S]*?</think(?:ing)?>" not in source, rel
# -- email compose markdown links (email_routes._md_to_email_html) -----------
_LINK_REF = re.compile(r"\[([^\]]+)\]\((https?://[^)\s]+)\)")
@pytest.mark.parametrize("text", [
"see [docs](https://example.com/a) and [b](http://x.y)",
"[a[b](http://x)",
"[]()[x](http://y)",
"[a](http://x [b c](http://y)",
"[a](http://)[b](https://z)",
"[a](ftp://x) [b](https://ok)",
"[unterminated(http://x)",
"[t](http://u) tail ] [",
])
def test_md_links_to_html_matches_reference(text):
from routes.email.email_routes import _md_links_to_html
assert _md_links_to_html(text) == _LINK_REF.sub(r'<a href="\2">\1</a>', text)
@pytest.mark.parametrize("flood", [
"[" + "[\\" * 40_000, # CodeQL's reported shape
"[a](http://x" * 8_000, # URL that never closes, repeated
])
def test_md_links_to_html_floods_are_linear(flood):
from routes.email.email_routes import _md_links_to_html
assert _fast(_md_links_to_html, flood) == flood
def test_md_to_email_html_still_renders_and_escapes_links():
from routes.email.email_routes import _md_to_email_html
html = _md_to_email_html("**hi** [site](https://example.com/x)\n<script>")
assert '<a href="https://example.com/x">site</a>' in html
assert "<strong>hi</strong>" in html
assert "<script>" not in html
# -- PDF URL detection (chat_routes._prefers_structured_document_tools) ------
_PDF_REF = re.compile(r"https?://[^\s]+(?:\.pdf\b|/pdf/)", re.I)
@pytest.mark.parametrize("text", [
"read https://arxiv.org/pdf/2401.0001 please",
"https://x.org/paper.PDF",
"https://x.org/paper.pdfx",
"xhttps://a/b.pdf",
"http:///pdf/",
"http://a
.pdf",
"no links here.pdf",
"https://a/b http://c.pdf",
])
def test_mentions_pdf_url_matches_reference(text):
from routes.chat_routes import _mentions_pdf_url
assert _mentions_pdf_url(text) is bool(_PDF_REF.search(text))
def test_mentions_pdf_url_scheme_flood_is_linear():
from routes.chat_routes import _prefers_structured_document_tools
assert _fast(_prefers_structured_document_tools, "http://" * 8_000) is False
# -- chat_helpers persistence cleanup ----------------------------------------
def test_clean_repeated_assistant_content_trailing_think_closer():
from routes.chat_helpers import clean_repeated_assistant_content
prose = "word " * 30 + "done" # past the 120-char edge-closer window
assert clean_repeated_assistant_content(prose + " </think> ") == prose
whitespace_run = "a" + "\t" * 80_000 + "x"
assert _fast(clean_repeated_assistant_content, whitespace_run) == whitespace_run
@pytest.mark.parametrize("text,expected", [
("The user wants a hi.\n\n<think>Hey there!</think>",
"<think>The user wants a hi.</think>\nHey there!"),
("I should greet.\n<thinking> Hello ", "<think>I should greet.</think>\nHello"),
("Plain answer <think>x</think>", "Plain answer <think>x</think>"),
])
def test_normalize_thinking_garbled_tags_unchanged(text, expected):
from routes.chat_helpers import _normalize_thinking
assert _normalize_thinking(text) == expected
@pytest.mark.parametrize("flood", [
"a" + "\n" * 80_000 + "x",
"The user x\n<think>" + "\t" * 80_000 + "x",
])
def test_normalize_thinking_floods_are_linear(flood):
from routes.chat_helpers import _normalize_thinking
_fast(_normalize_thinking, flood)
# -- calendar natural-language datetimes (GET /events, POST /events) ---------
def test_calendar_time_first_phrases_still_parse():
from routes.calendar_routes import _parse_dt, parse_due_for_user
parsed = _parse_dt("3pm tomorrow")
assert (parsed.hour, parsed.minute) == (15, 0)
assert "T09:00:00" in parse_due_for_user("9 a.m today")
assert "T23:00:00" in parse_due_for_user("11pm tonight")
@pytest.mark.parametrize("payload", [
"a" + "\t" * 80_000 + "x", # CodeQL's reported shape (time-first regex)
"a" + "\t" * 80_000 + "m", # am/pm normaliser on the same input
])
def test_calendar_parsers_whitespace_flood_is_linear(payload):
from routes.calendar_routes import _parse_dt, parse_due_for_user
for fn in (_parse_dt, parse_due_for_user):
started = time.perf_counter()
try:
fn(payload)
except Exception:
pass # unparseable is fine; only the time matters here
assert time.perf_counter() - started < _BUDGET_S, fn.__name__
+5 -3
View File
@@ -984,7 +984,7 @@ def test_attachment_extract_dir_stays_contained(folder, uid):
"""User-controlled folder/uid must never escape ATTACHMENTS_DIR — pins the
fix for the attachment-extraction path traversal."""
aed, base = _import_attachment_extract_dir()
target = aed(folder, uid)
target = aed(folder, uid, owner="../../owner", account_id="../acct")
base_r = base.resolve()
assert target == base_r or base_r in target.parents
# exactly one extra path segment, and no `..` component survived
@@ -992,9 +992,11 @@ def test_attachment_extract_dir_stays_contained(folder, uid):
assert ".." not in rel.parts
def test_attachment_extract_dir_normal_inputs_unchanged():
def test_attachment_extract_dir_is_stable_single_segment_per_scope():
aed, base = _import_attachment_extract_dir()
assert aed("INBOX", "123") == base.resolve() / "INBOX_123"
target = aed("INBOX", "123", owner="alice", account_id="acct-1")
assert target == aed("INBOX", "123", owner="alice", account_id="acct-1")
assert target.parent == base.resolve()
def test_diagnostics_routes_are_admin_gated():