From dcf4da4371d5a0e77b8de73fede89d3340448092 Mon Sep 17 00:00:00 2001 From: seonghobae <8172694+seonghobae@users.noreply.github.com> Date: Sat, 11 Jul 2026 17:15:04 +0000 Subject: [PATCH] =?UTF-8?q?=F0=9F=9B=A1=EF=B8=8F=20Sentinel:=20[HIGH]=20SS?= =?UTF-8?q?RF=20=EC=9A=B0=ED=9A=8C=20=EC=B7=A8=EC=95=BD=EC=A0=90=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 🚨 Severity: HIGH πŸ’‘ Vulnerability: `urllib.request.urlopen`이 λ¦¬λ‹€μ΄λ ‰νŠΈλ₯Ό 기본적으둜 따라가며, 이λ₯Ό λ°©μ§€ν•˜κΈ° μœ„ν•œ `NoRedirectHandler`κ°€ `HTTPErrorProcessor`λ₯Ό 상속받아 κ΅¬ν˜„λ˜μ–΄ μžˆμ—ˆμŠ΅λ‹ˆλ‹€. μ΄λŠ” λ¦¬λ‹€μ΄λ ‰νŠΈλŠ” λ§‰μ§€λ§Œ, 401μ΄λ‚˜ 404, 500κ³Ό 같은 λ‹€λ₯Έ λͺ¨λ“  HTTP μ—λŸ¬λ₯Ό μ–΅μ œν•˜μ—¬ μ„±κ³΅ν•œ μ‘λ‹΅μœΌλ‘œ μ²˜λ¦¬ν•˜λŠ” λΆ€μž‘μš©μ΄ μžˆμ—ˆμŠ΅λ‹ˆλ‹€. 🎯 Impact: μ˜¬λ°”λ₯Έ HTTP μƒνƒœ μ½”λ“œκ°€ λ¬΄μ‹œλ˜μ–΄ 예기치 μ•Šμ€ λ™μž‘μ΄λ‚˜ λ³΄μ•ˆ 검증 μš°νšŒκ°€ λ°œμƒν•  수 μžˆμŠ΅λ‹ˆλ‹€. πŸ”§ Fix: `NoRedirectHandler`κ°€ `urllib.request.HTTPRedirectHandler`λ₯Ό 상속받도둝 λ³€κ²½ν•˜κ³ , λ¦¬λ‹€μ΄λ ‰νŠΈ λ°œμƒ μ‹œ λͺ…μ‹œμ μœΌλ‘œ `urllib.error.HTTPError`λ₯Ό λ°œμƒμ‹œν‚€λ„λ‘ μˆ˜μ •ν–ˆμŠ΅λ‹ˆλ‹€. βœ… Verification: λ‹¨μœ„ ν…ŒμŠ€νŠΈ μΆ”κ°€ 및 `pytest` 톡과, 100% ν…ŒμŠ€νŠΈ 컀버리지 달성을 ν™•μΈν–ˆμŠ΅λ‹ˆλ‹€. --- scripts/ci/sandboxed_web_e2e.py | 10 ++++------ tests/test_sandboxed_web_e2e.py | 14 +++++++------- 2 files changed, 11 insertions(+), 13 deletions(-) diff --git a/scripts/ci/sandboxed_web_e2e.py b/scripts/ci/sandboxed_web_e2e.py index 682202fa..506cc044 100644 --- a/scripts/ci/sandboxed_web_e2e.py +++ b/scripts/ci/sandboxed_web_e2e.py @@ -26,14 +26,12 @@ RESULT_MARKER = "SANDBOXED_WEB_E2E_RESULT" -class NoRedirectHandler(urllib.request.HTTPErrorProcessor): +class NoRedirectHandler(urllib.request.HTTPRedirectHandler): """Explicitly disable redirects to prevent SSRF bypasses via 301/302 to local IPs.""" - def http_response(self, request, response): - """Return the original HTTP response without following redirects.""" - return response - - https_response = http_response + def redirect_request(self, req, fp, code, msg, headers, newurl): + """Raise an HTTPError instead of following the redirect.""" + raise urllib.error.HTTPError(req.full_url, code, msg, headers, fp) @dataclass diff --git a/tests/test_sandboxed_web_e2e.py b/tests/test_sandboxed_web_e2e.py index 38f5f8bf..7bd9a2a8 100644 --- a/tests/test_sandboxed_web_e2e.py +++ b/tests/test_sandboxed_web_e2e.py @@ -228,16 +228,16 @@ def open(self, url, timeout): assert sandboxed_web_e2e.tail_text(log_path).splitlines()[0] == "line-10" -def test_no_redirect_handler_returns_redirect_without_following(): - """Readiness checks must not follow redirects to attacker-controlled internal URLs.""" - - class RedirectResponse: - status = 302 +def test_no_redirect_handler_raises_httperror_without_following(): + """Readiness checks must raise HTTPError on redirects to prevent attacker-controlled internal URLs.""" + import urllib.error request = sandboxed_web_e2e.urllib.request.Request("https://example.test/ready") - response = RedirectResponse() - assert sandboxed_web_e2e.NoRedirectHandler().http_response(request, response) is response + with pytest.raises(urllib.error.HTTPError) as exc_info: + sandboxed_web_e2e.NoRedirectHandler().redirect_request(request, None, 302, "Found", {}, "http://127.0.0.1") + + assert exc_info.value.code == 302 def test_wait_for_url_returns_false_after_timeout(monkeypatch, tmp_path):