Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions scripts/ci/noema_review_gate.py
Original file line number Diff line number Diff line change
Expand Up @@ -299,6 +299,12 @@ def call_llm(repo: str, number: int, pr: dict[str, Any], diff: str, truncated: b
if ip.is_private or ip.is_loopback or ip.is_link_local or ip.is_multicast or ip.is_unspecified:
raise ValueError("URL cannot target internal IP addresses")

if not (api_url.lower().startswith("http://") or api_url.lower().startswith("https://")):
raise ValueError(
"NOEMA_LLM_API_URL must start with http:// or https:// "
"to prevent SSRF vulnerabilities"
)

prompt = {
"role": "user",
"content": "\n".join(
Expand Down
19 changes: 19 additions & 0 deletions tests/test_noema_review_gate.py
Original file line number Diff line number Diff line change
Expand Up @@ -286,6 +286,25 @@ def fake_getaddrinfo_invalid_ip(host, port, *args, **kwargs):
assert noema.call_llm("owner/repo", 1, pr, "diff", True)["decision"] == "approve"


def test_call_llm_rejects_control_character_scheme_evasion(monkeypatch):
"""A URL with an embedded tab is normalized by urlparse to an http scheme
with a valid hostname, but its raw form does not start with http:// — the
startswith guard must still reject it to prevent SSRF via control-character
scheme evasion."""
pr = make_pr()
monkeypatch.setenv("NOEMA_LLM_API_KEY", "secret")
monkeypatch.setenv("NOEMA_LLM_API_URL", "http\t://sneaky.example.com/chat")

import socket

def raise_gaierror(host, port, *args, **kwargs):
raise socket.gaierror("Name or service not known")

monkeypatch.setattr(socket, "getaddrinfo", raise_gaierror)
with pytest.raises(ValueError, match="must start with http:// or https://"):
noema.call_llm("owner/repo", 1, pr, "diff", False)


def test_format_findings_and_submit_review(monkeypatch):
findings = noema.format_findings(
[
Expand Down
Loading