-
Notifications
You must be signed in to change notification settings - Fork 0
fix(e2e): restrict readiness polling to loopback destinations #1314
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
Merged
seonghobae
merged 25 commits into
main
from
cursor/bc-deb78137-73b8-43fe-bfe5-af265490f12a-4f61
Aug 29, 2026
Merged
Changes from all commits
Commits
Show all changes
25 commits
Select commit
Hold shift + click to select a range
9a47c10
fix(e2e): restrict readiness polling to loopback destinations
cursoragent 3f0c3ac
fix(e2e): resolve localhost and require loopback answers
cursoragent 0ef296d
fix(strix): strip MODEL QUALITY WARNING boxes without fail-open
cursoragent 9397bf4
fix(strix): keep same-box failure evidence when stripping the banner …
cursoragent c1ab957
fix(strix): match the banner heading anywhere on the line
cursoragent f02eb90
fix(strix): delete cosmetic-only banner boxes, keep same-box failures
cursoragent 51291c3
fix(strix): align overlay to gpt-5.4 and keep same-box rate-limit evi…
cursoragent 6b06a92
fix(strix): align gpt-5.4 contracts and expect rate-limit retries
cursoragent d025cc1
merge(main): make #1314 a descendant of 8fd471a3 for exact-head
cursoragent d4ea752
fix(strix): keep OpenCode dispatch-pool harness needles on gpt-5.6-luna
cursoragent 006c523
merge(main): make #1314 a descendant of d2c554db after #1320
cursoragent 58789e6
Merge branch 'main' into cursor/bc-deb78137-73b8-43fe-bfe5-af265490f1…
seonghobae 7d3dddd
fix(strix): ignore known internal console warnings
seonghobae 908f794
fix(strix): preserve malformed console failure markers
seonghobae 9f267b8
fix(strix): invalidate stale classified console copies
seonghobae 0f0adf8
Merge branch 'main' into cursor/bc-deb78137-73b8-43fe-bfe5-af265490f1…
seonghobae e294587
docs(copy): make web verification guidance actionable
seonghobae 460c321
Merge branch 'main' into cursor/bc-deb78137-73b8-43fe-bfe5-af265490f1…
opencode-agent[bot] e13fdda
Merge branch 'main' into cursor/bc-deb78137-73b8-43fe-bfe5-af265490f1…
opencode-agent[bot] 6f6573e
Merge branch 'main' into cursor/bc-deb78137-73b8-43fe-bfe5-af265490f1…
opencode-agent[bot] dd16e95
Merge branch 'main' into cursor/bc-deb78137-73b8-43fe-bfe5-af265490f1…
opencode-agent[bot] 393852c
Merge remote-tracking branch 'origin/main' into HEAD
seonghobae 3525287
refactor(e2e): isolate readiness SSRF repair
seonghobae 084d4e3
docs(adr): give readiness boundary a unique identity
seonghobae c678cc0
Merge branch 'main' into cursor/bc-deb78137-73b8-43fe-bfe5-af265490f1…
seonghobae File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,9 @@ | ||
| # ADR-0004: Sandboxed web readiness loopback boundary | ||
|
|
||
| - Status: accepted | ||
| - Date: 2026-08-25 | ||
| - Scope: ContextualWisdomLab/.github control-plane E2E sandbox | ||
| - Decision: Poll `--backend-ready-url` and `--frontend-ready-url` only after the URL is proven to be HTTP(S) loopback. Accept literal `localhost` or a standard-library loopback address, resolve `localhost` and require every answer to be loopback, unwrap IPv4-mapped IPv6, reject userinfo and missing hosts, and keep redirects disabled. | ||
| - Ownership: `.github` owns the sandbox helper. Product repositories keep pointing readiness at their own loopback services. | ||
| - Figma File ID: N/A. This repository has no customer UI. | ||
| - Consequence: A review run cannot use the sandbox poller as an SSRF trampoline to metadata services or public hosts. Operators fix a rejected URL by pointing it at `127.0.0.1` or `::1`. Papers live in `docs/doctoring/sandboxed-web-readiness-loopback-boundary.md`. |
85 changes: 85 additions & 0 deletions
85
docs/doctoring/sandboxed-web-readiness-loopback-boundary.md
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,85 @@ | ||
| # Sandboxed web readiness loopback boundary | ||
|
|
||
| ## Decision | ||
|
|
||
| `sandboxed_web_e2e.py` opens a readiness URL only after | ||
| `require_loopback_readiness_url` accepts it. The accepted destinations are | ||
| literal `localhost` (a trailing FQDN dot is stripped) or an address that | ||
| Python's standard-library `ipaddress` module classifies as loopback after | ||
| IPv4-mapped IPv6 addresses are unwrapped. Literal `localhost` is then | ||
| resolved; every A/AAAA answer must itself be loopback, so a poisoned hosts | ||
| file cannot smuggle a public address through the name allowlist. Redirects | ||
| remain disabled. | ||
|
|
||
| This supports the complete IPv4 loopback block, including `127.0.0.2`, and | ||
| IPv6 `::1`. It rejects `0.0.0.0`, `::`, public hosts, `.localhost` | ||
| subdomains, cloud-metadata link-local addresses, missing hosts, and | ||
| userinfo-confused URLs such as `http://user@127.0.0.1/`. A mapped public | ||
| address such as `::ffff:8.8.8.8` cannot pass merely because it is IPv6. | ||
|
|
||
| The boundary uses the standard library rather than a second address table. | ||
| It therefore follows the runtime's maintained special-purpose definitions and | ||
| keeps one fail-closed validation point before any network request. Do not add | ||
| individual non-loopback exceptions. | ||
|
|
||
| This successor lands the same buyer-facing repair as ContextualWisdomLab/.github#1244 | ||
| on current `main` and keeps Strix classifier ownership out of the SSRF slice | ||
| (unlike ContextualWisdomLab/.github#1313). | ||
|
|
||
| ## Operator next action | ||
|
|
||
| Point `--backend-ready-url` and `--frontend-ready-url` at the sandboxed | ||
| service on loopback. If readiness fails with `URL cannot target external | ||
| hostname`, replace the destination with `http://127.0.0.1:<port>/...` or | ||
| `http://[::1]:<port>/...` instead of opening the firewall or adding a | ||
| hostname exception. | ||
|
|
||
| ## Verification | ||
|
|
||
| The regression exercises literal `localhost`, a trailing-dot `localhost.`, | ||
| `127.0.0.1`, another address in `127.0.0.0/8`, IPv6 `::1`, mapped loopback | ||
| `::ffff:127.0.0.1`, an unspecified address, a `.localhost` subdomain, a | ||
| public hostname, the common cloud metadata address, mapped public IPv6, | ||
| userinfo, a missing host, and poisoned localhost resolution (public A, | ||
| mapped public AAAA, empty answers, resolver errors, and non-IP answers). | ||
| The existing no-redirect test continues to prove that an allowed readiness | ||
| endpoint cannot redirect the poller across the boundary. | ||
|
|
||
| ```mermaid | ||
| flowchart TD | ||
| Url["Readiness URL"] | ||
| Scheme{"http or https?"} | ||
| Userinfo{"userinfo present?"} | ||
| Host{"loopback IP, or localhost whose every resolved answer is loopback?"} | ||
| Open["Poll with redirects disabled"] | ||
| Reject["Fail closed before any request"] | ||
|
|
||
| Url --> Scheme | ||
| Scheme -->|"no"| Reject | ||
| Scheme -->|"yes"| Userinfo | ||
| Userinfo -->|"yes"| Reject | ||
| Userinfo -->|"no"| Host | ||
| Host -->|"no"| Reject | ||
| Host -->|"yes"| Open | ||
| ``` | ||
|
|
||
| ## References | ||
|
|
||
| Berners-Lee, T., Fielding, R., & Masinter, L. (2005). *Uniform Resource | ||
| Identifier (URI): Generic syntax* (RFC 3986). Internet Engineering Task | ||
| Force. https://doi.org/10.17487/RFC3986 | ||
|
|
||
| Cotton, B., Vegoda, L., Bonica, R., & Haberman, B. (2013). *Special-purpose | ||
| IP address registries* (RFC 6890). Internet Engineering Task Force. | ||
| https://doi.org/10.17487/RFC6890 | ||
|
|
||
| Internet Assigned Numbers Authority. (2026). *IANA IPv4 special-purpose | ||
| address registry*. Retrieved August 25, 2026, from | ||
| https://www.iana.org/assignments/iana-ipv4-special-registry/iana-ipv4-special-registry.xhtml | ||
|
|
||
| OWASP Foundation. (n.d.). *Server-side request forgery prevention cheat | ||
| sheet*. Retrieved August 25, 2026, from | ||
| https://cheatsheetseries.owasp.org/cheatsheets/Server_Side_Request_Forgery_Prevention_Cheat_Sheet.html | ||
|
|
||
| Python Software Foundation. (2026). *ipaddress — IPv4/IPv6 manipulation | ||
| library*. https://docs.python.org/3/library/ipaddress.html |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -3,16 +3,19 @@ | |
| from __future__ import annotations | ||
|
|
||
| import argparse | ||
| import ipaddress | ||
| import json | ||
| import os | ||
| import signal | ||
| import shutil | ||
| import shlex | ||
| import socket | ||
| import subprocess | ||
| import sys | ||
| import tempfile | ||
| import time | ||
| import urllib.error | ||
| import urllib.parse | ||
| import urllib.request | ||
| from collections.abc import Sequence | ||
| from dataclasses import dataclass | ||
|
|
@@ -115,12 +118,61 @@ def start_service(label: str, command: str, cwd: Path, env: dict[str, str], logs | |
| return Service(label=label, command=command, process=process, log_path=log_path) | ||
|
|
||
|
|
||
| def _require_loopback_ip_text(ip_text: str, hostname: str) -> None: | ||
| """Reject a literal or resolved address that is not loopback.""" | ||
| try: | ||
| address = ipaddress.ip_address(ip_text) | ||
| except ValueError as exc: | ||
| raise ValueError(f"URL cannot target external hostname: {hostname}") from exc | ||
| if address.version == 6 and address.ipv4_mapped is not None: | ||
| address = address.ipv4_mapped | ||
| if not address.is_loopback: | ||
| raise ValueError(f"URL cannot target external hostname: {hostname}") | ||
|
|
||
|
|
||
| def _require_resolved_loopback_hostname(hostname: str) -> None: | ||
| """Resolve a literal localhost name and require every answer to be loopback.""" | ||
| try: | ||
| results = socket.getaddrinfo(hostname, None) | ||
| except OSError as exc: | ||
| raise ValueError(f"URL cannot target unresolved hostname: {hostname}") from exc | ||
| if not results: | ||
| raise ValueError(f"URL cannot target unresolved hostname: {hostname}") | ||
| for result in results: | ||
| _require_loopback_ip_text(result[4][0], hostname) | ||
|
seonghobae marked this conversation as resolved.
|
||
|
|
||
|
|
||
| def require_loopback_readiness_url(url: str) -> None: | ||
| """Reject a readiness URL that is not a local loopback HTTP(S) target. | ||
|
|
||
| Operators should point ``--backend-ready-url`` and ``--frontend-ready-url`` | ||
| at the sandboxed service itself. Public hosts, cloud metadata addresses, | ||
| unspecified bind addresses, DNS names other than literal ``localhost``, | ||
| and userinfo-confused URLs are rejected before any request is opened. | ||
| Literal ``localhost`` is resolved and every answer must be loopback, so a | ||
| poisoned hosts file cannot smuggle a public A/AAAA record through the | ||
| name allowlist. IPv4-mapped IPv6 addresses are unwrapped and re-checked | ||
| so ``::ffff:8.8.8.8`` cannot bypass the loopback rule. | ||
| """ | ||
| parsed = urllib.parse.urlparse(url) | ||
| if parsed.scheme.lower() not in {"http", "https"}: | ||
| raise ValueError(f"URL must start with http:// or https://, got: {url}") | ||
| if parsed.username or parsed.password: | ||
| raise ValueError("URL cannot include userinfo") | ||
| hostname = (parsed.hostname or "").lower().rstrip(".") | ||
| if not hostname: | ||
| raise ValueError("URL must include a loopback hostname") | ||
| if hostname == "localhost": | ||
| _require_resolved_loopback_hostname(hostname) | ||
| return | ||
|
devin-ai-integration[bot] marked this conversation as resolved.
|
||
| _require_loopback_ip_text(hostname, hostname) | ||
|
cursor[bot] marked this conversation as resolved.
seonghobae marked this conversation as resolved.
|
||
|
|
||
|
|
||
| def wait_for_url(url: str, timeout: int, service: Service) -> bool: | ||
| """Poll a readiness URL until it responds or the service exits.""" | ||
| if not url: | ||
| return True | ||
| if not (url.startswith("http://") or url.startswith("https://")): | ||
| raise ValueError(f"URL must start with http:// or https://, got: {url}") | ||
| require_loopback_readiness_url(url) | ||
|
Comment on lines
171
to
+175
Contributor
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. |
||
| deadline = time.monotonic() + timeout | ||
| opener = urllib.request.build_opener(NoRedirectHandler()) | ||
| while time.monotonic() < deadline: | ||
|
|
||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
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.
📝 Info: Mapped addresses share one classifier
_require_loopback_ip_textunwraps mapped IPv6 before classification for both literal hosts and resolver answers. This keeps both validation paths consistent.Was this helpful? React with 👍 or 👎 to provide feedback.