-
Notifications
You must be signed in to change notification settings - Fork 0
fix: refuse a detector address that is not https #16
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,8 +1,9 @@ | ||
| """Configuration and secrets supplied by the environment.""" | ||
|
|
||
| from typing import Annotated | ||
| from urllib.parse import urlparse | ||
|
|
||
| from pydantic import Field | ||
| from pydantic import Field, field_validator | ||
| from pydantic_settings import BaseSettings, SettingsConfigDict | ||
|
|
||
|
|
||
|
|
@@ -67,5 +68,37 @@ class Settings(BaseSettings): | |
| portkey_api_key: str = "" | ||
| portkey_virtual_key: str = "" | ||
|
|
||
| @field_validator("llm_guardrail_base_url") | ||
| @classmethod | ||
| def _remote_detector_must_be_https(cls, url: str) -> str: | ||
| """Refuse a remote detector address that is not https. | ||
|
|
||
| A detector behind ingress that refuses plain HTTP answers it with a | ||
| redirect. Readiness still passes, because the probe follows the | ||
| redirect and receives a 200, so the worker wakes the GPU and only then | ||
| fails every screening: following a redirect turns the guardrail's POST | ||
| into a GET, which the endpoint rejects. Refusing the address at startup | ||
| costs a clear error instead of a cold start. | ||
|
|
||
| A local address is exempt, having no ingress in front of it. | ||
|
|
||
| Args: | ||
| url: The configured detector address. | ||
|
|
||
| Returns: | ||
| The address, unchanged. | ||
|
|
||
| Raises: | ||
| ValueError: The address is remote and does not use https. | ||
| """ | ||
| host = urlparse(url).hostname or "" | ||
| if host in ("localhost", "127.0.0.1", "::1"): | ||
| return url | ||
| if not url.startswith("https://"): | ||
| raise ValueError( | ||
| f"llm_guardrail_base_url must be https for a remote host, got {url!r}" | ||
| ) | ||
|
Comment on lines
+97
to
+100
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win 🧩 Analysis chain🏁 Script executed: #!/bin/bash
set -eu
printf '%s\n' '--- app/config.py ---'
sed -n '1,150p' app/config.py
printf '%s\n' '--- URL validation references ---'
rg -n -C 3 'llm_guardrail_base_url|localhost|127\.0\.0\.1|loopback|https://' . \
-g '!node_modules' -g '!dist' -g '!build' \
| head -n 300
printf '%s\n' '--- candidate tests ---'
fd -t f -i 'test|spec' . | head -n 100Repository: inesaranab/screening Length of output: 36917 🏁 Script executed: #!/bin/bash
set -eu
printf '%s\n' '--- tests/unit/test_config.py ---'
cat -n tests/unit/test_config.py
printf '%s\n' '--- URL parser behavior for reported cases ---'
python3 - <<'PY'
from ipaddress import ip_address
from urllib.parse import urlparse
urls = [
"HTTPS://example.com/v1",
"https:///v1",
"file://localhost/v1",
"http://127.0.0.2:8001/v1",
"http://[::2]:8001/v1",
"http://localhost:8001/v1",
"http://[::1]:8001/v1",
"https://example.com/v1",
]
def current(url):
host = urlparse(url).hostname or ""
if host in ("localhost", "127.0.0.1", "::1"):
return "allow-local"
if not url.startswith("https://"):
return "reject-remote-http"
return "allow-remote"
def parsed_policy(url):
parsed = urlparse(url)
host = parsed.hostname
if not host or parsed.scheme.lower() not in {"http", "https"}:
return "reject-invalid"
try:
local = host.lower() == "localhost" or ip_address(host).is_loopback
except ValueError:
local = False
if local:
return "allow-local-http-or-https"
return "allow-remote-https" if parsed.scheme.lower() == "https" else "reject-remote-http"
for url in urls:
parsed = urlparse(url)
print({
"url": url,
"scheme": parsed.scheme,
"hostname": parsed.hostname,
"current": current(url),
"parsed_policy": parsed_policy(url),
})
PYRepository: inesaranab/screening Length of output: 4505 Validate parsed URL components, not a string prefix. Parse the URL once and require a hostname. Allow only 🤖 Prompt for AI Agents |
||
| return url | ||
|
|
||
|
|
||
| settings = Settings() # type: ignore[call-arg] | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -34,3 +34,28 @@ def test_jobs_storage_defaults_point_at_the_general_purpose_account(): | |
| assert "screeningjobs" in Settings.model_fields["jobs_account_url"].default | ||
| assert Settings.model_fields["jobs_table_name"].default | ||
| assert Settings.model_fields["jobs_queue_name"].default | ||
|
|
||
|
|
||
| def test_a_plain_http_detector_address_is_refused(): | ||
| """The detector's ingress answers plain HTTP with a redirect rather than | ||
| serving it, and a client following that redirect turns the POST into a GET, | ||
| which the endpoint rejects with 405. Readiness still passes -- the probe | ||
| follows the redirect and gets a 200 -- so the failure arrives only after a | ||
| cold start has been paid for.""" | ||
| import pytest | ||
| from pydantic import ValidationError | ||
|
|
||
| from app.config import Settings | ||
|
|
||
| with pytest.raises(ValidationError): | ||
| Settings(llm_guardrail_base_url="http://screening-gemma.internal.example/v1") | ||
|
Comment on lines
+50
to
+51
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win Pass the required test credential explicitly in both settings tests. The
🧰 Tools🪛 ast-grep (0.45.1)[warning] 50-50: Do not make http calls without encryption (requests-http) 📍 Affects 1 file
🤖 Prompt for AI Agents |
||
|
|
||
|
|
||
| def test_a_local_http_detector_address_is_allowed(): | ||
| """A detector served on localhost has no ingress in front of it, so the | ||
| scheme carries none of the same risk.""" | ||
| from app.config import Settings | ||
|
|
||
| settings = Settings(llm_guardrail_base_url="http://localhost:8001/v1") | ||
|
|
||
| assert settings.llm_guardrail_base_url.startswith("http://") | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
Repository: inesaranab/screening
Length of output: 25056
🏁 Script executed:
Repository: inesaranab/screening
Length of output: 3813
Recognize the full loopback range.
The validator rejects valid loopback addresses such as
127.0.0.2. Useipaddress.ip_address(host).is_loopbackand retainlocalhostas the name exception.🤖 Prompt for AI Agents