SecLab

SSRF in Django's URL validator — upgrade to 5.2.4

URLValidator accepts several alternate IP encodings, letting hand-written allowlist checks be bypassed.

22 thg 8, 20263 phút đọcAI soạn nhápcurate.summarizeĐã qua editor duyệtChưa có bản tiếng Việt

What happened

Django 5.2.4 fixes a flaw in URLValidator: the validator accepts several alternate IP encodings that most hand-written allowlist checks never consider. The result is a URL that looks harmless, passes your check, and then points at your internal network.

The bug is not that Django "allows SSRF" — URLValidator was never an SSRF control. The problem is that a great many codebases use it as though it were.

If your application accepts a user-supplied URL and fetches it, keep reading even if you do not use Django. The same class of bug lives in .NET's Uri, Node's URL and Go's net/url.

The mechanism

All four strings below resolve to loopback, and a string-comparison check misses every one:

StringResolves toWhy it slips through
http://127.1/127.0.0.1A valid shorthand IPv4 form
http://2130706433/127.0.0.1IPv4 as a 32-bit integer
http://017700000001/127.0.0.1Octal notation
http://[::1]/::1IPv6 loopback; an IPv4 blocklist never touches it

There is a further variant that has nothing to do with notation and everything to do with timing:

1. Validator resolves evil.test  →  93.184.216.34  (public, fine)  → allowed2. HTTP library resolves AGAIN   →  127.0.0.1      (DNS changed, TTL 0)3. The real request lands on loopback

That is DNS rebinding, and it is why validating the hostname before the call is never enough.

What to do

If you use Django

Upgrade to >= 5.2.4:

Shell
pip index versions djangopip install --upgrade "django>=5.2.4"python -c "import django; print(django.get_version())"

If you cannot upgrade yet

Block at the connection layer, not the string layer:

Python
import ipaddress, socketfrom urllib.parse import urlparse ALLOWED_HOSTS = {"api.partner.example", "cdn.partner.example"} def safe_target(raw: str) -> tuple[str, int]:    u = urlparse(raw)    if u.scheme != "https" or u.hostname not in ALLOWED_HOSTS:        raise ValueError("destination not allowed")     # Resolve ONCE, then connect to the exact IP you checked.    infos = socket.getaddrinfo(u.hostname, u.port or 443, proto=socket.IPPROTO_TCP)    ip = ipaddress.ip_address(infos[0][4][0])    if not ip.is_global:        raise ValueError(f"blocked destination {ip}")    return str(ip), u.port or 443

Do not reach for a blocklist instead. 127.0.0.1, localhost, 0.0.0.0, [::1], 2130706433, and any domain you do not control that points at loopback — the deny list is always missing one entry.

The layer that does not depend on your code

Egress deny-by-default at the network layer. Outbound-calling services run in their own network with no view of the metadata endpoint or internal services. With that in place, an SSRF that slips past the code becomes a log line rather than an incident.

Verifying the fix

The suite must fail on the old code and pass after the patch — otherwise it proves nothing:

Python
import pytest @pytest.mark.parametrize("url", [    "http://169.254.169.254/latest/meta-data/",    "http://127.1/", "http://2130706433/", "http://[::1]/",    "https://rebind.test/",          # DNS returns 127.0.0.1])def test_preview_rejects_internal(client, url):    assert client.post("/preview", json={"url": url}).status_code == 400

These tests do not catch SSRF over other schemes if you allow more than http/https, nor the case where the HTTP library re-resolves — to stop that you must pin the IP as shown above.

Back to the fundamentals

This class of bug is API7 (SSRF) in the API Security Top 10 and C10 in the Proactive Controls. If that ground is not solid for you yet, session W1D5 in the path walks all four variants above and ends with the patch.

Nguồn tham khảo

Bình luận

Tham gia thảo luận
Đăng ký để bình luận

Bình luận cần tài khoản đã hoàn thành ít nhất một bài học. Điều kiện đó là thứ giữ cho luồng thảo luận này còn đáng đọc: mỗi ý kiến gắn với một người có thể bị hỏi lại, và reputation tích luỹ theo thời gian.

Đăng kýĐăng nhập

Bạn vẫn đọc được toàn bộ bình luận dưới đây mà không cần tài khoản. Đăng nhập xong bạn sẽ quay lại đúng chỗ này, không phải đầu trang.

Đang tải bình luận…