|
9 | 9 | from typing import Any |
10 | 10 |
|
11 | 11 | from django.utils.deprecation import MiddlewareMixin |
| 12 | +from django.conf import settings |
12 | 13 |
|
13 | 14 | logger = logging.getLogger("epiportal.requests") |
14 | 15 |
|
@@ -43,13 +44,35 @@ def _should_log_request(request) -> bool: |
43 | 44 | return not any(pattern in path for pattern in LOG_EXCLUDE_PATH_PATTERNS) |
44 | 45 |
|
45 | 46 |
|
46 | | -def _get_client_ip(request) -> str: |
| 47 | +def _get_client_ip(req) -> str: |
47 | 48 | """Extract client IP, respecting X-Forwarded-For when behind proxies.""" |
48 | | - x_forwarded_for = request.META.get("HTTP_X_FORWARDED_FOR") |
49 | | - if x_forwarded_for: |
50 | | - # Take the leftmost (original client) IP |
51 | | - return x_forwarded_for |
52 | | - return request.META.get("REMOTE_ADDR", "") |
| 49 | + if settings.REVERSE_PROXY_DEPTH: |
| 50 | + # we only expect/trust (up to) "REVERSE_PROXY_DEPTH" number of proxies between this server and the outside world. |
| 51 | + # a REVERSE_PROXY_DEPTH of 0 means not proxied, i.e. server is globally directly reachable. |
| 52 | + # a negative proxy depth is a special case to trust the whole chain -- not generally recommended unless the |
| 53 | + # most-external proxy is configured to disregard "X-Forwarded-For" from outside. |
| 54 | + # really, ONLY trust the following headers if reverse proxied!!! |
| 55 | + x_forwarded_for = req.META.get("HTTP_X_FORWARDED_FOR") |
| 56 | + |
| 57 | + if x_forwarded_for: |
| 58 | + full_proxy_chain = x_forwarded_for.split(",") |
| 59 | + # eliminate any extra addresses at the front of this list, as they could be spoofed. |
| 60 | + if settings.REVERSE_PROXY_DEPTH > 0: |
| 61 | + depth = settings.REVERSE_PROXY_DEPTH |
| 62 | + else: |
| 63 | + # special case for -1/negative: setting `depth` to 0 will not strip any items from the chain |
| 64 | + depth = 0 |
| 65 | + trusted_proxy_chain = full_proxy_chain[-depth:] |
| 66 | + # accept the first (or only) address in the remaining trusted part of the chain as the actual remote address |
| 67 | + return trusted_proxy_chain[0].strip() |
| 68 | + |
| 69 | + # fall back to "X-Real-Ip" if "X-Forwarded-For" isnt present |
| 70 | + x_real_ip = req.META.get("HTTP_X_REAL_IP") |
| 71 | + if x_real_ip: |
| 72 | + return x_real_ip |
| 73 | + |
| 74 | +# if we are not proxied (or we are proxied but the headers werent present and we fell through to here), just use the remote ip addr as the true client address |
| 75 | + return req.META.get("REMOTE_ADDR") |
53 | 76 |
|
54 | 77 |
|
55 | 78 | def _sanitize_headers(meta: dict) -> dict[str, str]: |
|
0 commit comments