mirror of
https://github.com/Screenly/Anthias.git
synced 2026-06-10 09:08:09 -04:00
fix: address self-review findings on PR #2757
* Gate SECURE_PROXY_SSL_HEADER on FORWARDED_ALLOW_IPS
(anthias_django/settings.py): without the gate, a client on a
plain-HTTP deploy could send `X-Forwarded-Proto: https` and flip
`request.is_secure()`. Django reads the header from META directly,
independent of uvicorn's --proxy-headers flag, so the previous
unconditional setting was actually exploitable in non-SSL mode
(secure-cookied sessions would drop on the next plain-HTTP request,
redirects would point at https:// URLs that don't exist).
Verified live: non-SSL → SECURE_PROXY_SSL_HEADER is None and
is_secure() with spoofed XFP=https returns False; SSL via Caddy
override → header is set and is_secure() returns True.
* Replace the isfile() pre-check + open() in anthias_assets and
static_with_mime with a try/except FileNotFoundError around open()
(anthias_app/views_files.py). Eliminates a (tiny but real) TOCTOU
window between the stat and the open. IsADirectoryError handled
too, since `realpath('/dir/')` resolves to the directory and open()
would otherwise 500.
* Comment FORWARDED_ALLOW_IPS=* assumption in bin/enable_ssl.sh: the
wildcard is only safe because the override drops anthias-server's
external port mapping, so any future edit that re-adds a host:port
publication has to either tighten the wildcard to Caddy's IP/CIDR
or unset it.
* Replace ANSI-C escape sequences in the Caddyfile generator with
plain multi-line strings. `read -r -d ''` was the first attempt
but it strips trailing newlines, which collapsed `auto_https off`
onto the same line as `}` in cert mode. Multi-line literals with
echo "$VAR" are unambiguous and Caddy validates all three modes
cleanly again.
* Add a docker-volume cleanup hint to bin/disable_ssl.sh: Caddy's
local CA persists in anthias_anthias-caddy-data so an enable →
disable → enable cycle reuses the same CA (intentional — browsers
that trusted it stay trusted), and operators who want a fresh CA
now have the exact `docker volume rm` command in the script's
output.
15 view tests still pass; default + SSL Caddyfiles still validate;
default + SSL endpoints still return 200 / 301 / 101 in smoke tests.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -57,13 +57,16 @@ def require_client_in(*cidrs):
|
||||
@require_GET
|
||||
@require_client_in(DOCKER_BRIDGE_CIDR)
|
||||
def anthias_assets(request, filename):
|
||||
# Trailing os.sep on `base` is required so e.g.
|
||||
# '/data/anthias_assets_evil/...' doesn't slip past startswith().
|
||||
base = os.path.realpath(ANTHIAS_ASSETS_ROOT) + os.sep
|
||||
target = os.path.realpath(os.path.join(base, filename))
|
||||
if not target.startswith(base):
|
||||
raise Http404
|
||||
if not os.path.isfile(target):
|
||||
try:
|
||||
return FileResponse(open(target, 'rb'))
|
||||
except (FileNotFoundError, IsADirectoryError):
|
||||
raise Http404
|
||||
return FileResponse(open(target, 'rb'))
|
||||
|
||||
|
||||
@require_GET
|
||||
@@ -73,12 +76,13 @@ def static_with_mime(request, filename):
|
||||
target = os.path.realpath(os.path.join(base, filename))
|
||||
if not target.startswith(base):
|
||||
raise Http404
|
||||
if not os.path.isfile(target):
|
||||
raise Http404
|
||||
content_type = request.GET.get('mime') or (
|
||||
mimetypes.guess_type(target)[0] or 'application/octet-stream'
|
||||
)
|
||||
return FileResponse(open(target, 'rb'), content_type=content_type)
|
||||
try:
|
||||
return FileResponse(open(target, 'rb'), content_type=content_type)
|
||||
except (FileNotFoundError, IsADirectoryError):
|
||||
raise Http404
|
||||
|
||||
|
||||
@require_GET
|
||||
|
||||
@@ -186,10 +186,14 @@ DATA_UPLOAD_MAX_MEMORY_SIZE = None
|
||||
FILE_UPLOAD_MAX_MEMORY_SIZE = 26_214_400
|
||||
|
||||
# Trust X-Forwarded-Proto from a TLS-terminating proxy (the Caddy
|
||||
# sidecar that bin/enable_ssl.sh installs). uvicorn enforces who is
|
||||
# allowed to set the header via FORWARDED_ALLOW_IPS — without that env
|
||||
# var a malicious client cannot make request.is_secure() lie.
|
||||
SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https')
|
||||
# sidecar that bin/enable_ssl.sh installs) only when uvicorn has been
|
||||
# told to honour proxy headers via FORWARDED_ALLOW_IPS. Without the
|
||||
# gate, any client could set X-Forwarded-Proto: https on a plain-HTTP
|
||||
# deploy and flip request.is_secure() — secure-cookied sessions would
|
||||
# then drop on the next plain-HTTP request, and redirects would point
|
||||
# at https:// URLs that don't exist.
|
||||
if getenv('FORWARDED_ALLOW_IPS'):
|
||||
SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https')
|
||||
|
||||
# Default primary key field type
|
||||
# https://docs.djangoproject.com/en/4.2/ref/settings/#default-auto-field
|
||||
|
||||
@@ -24,3 +24,10 @@ sudo -E docker compose \
|
||||
|
||||
echo
|
||||
echo "SSL disabled. Anthias is now reachable at http://<your IP> (port 80)."
|
||||
echo
|
||||
echo "Caddy's local CA + issued certs are kept in the docker volume"
|
||||
echo " anthias_anthias-caddy-data"
|
||||
echo "so re-enabling SSL with bin/enable_ssl.sh reuses the same CA"
|
||||
echo "(browsers that already trusted it stay trusted). To wipe the CA"
|
||||
echo "and start over, run:"
|
||||
echo " docker volume rm anthias_anthias-caddy-data anthias_anthias-caddy-config"
|
||||
|
||||
@@ -108,7 +108,7 @@ if [[ -n "$USER_CERT" ]]; then
|
||||
SITE_ADDRESS="${DOMAIN:-:443}"
|
||||
TLS_DIRECTIVE="tls /etc/anthias/ssl/cert.pem /etc/anthias/ssl/key.pem"
|
||||
# Disable Caddy's ACME path; we're serving the supplied cert.
|
||||
EXTRA_GLOBAL=$' auto_https off\n'
|
||||
EXTRA_GLOBAL=" auto_https off"
|
||||
MOUNT_SSL_DIR=1
|
||||
[[ -z "$DOMAIN" ]] && NEEDS_REDIRECT_BLOCK=1
|
||||
elif [[ -n "$DOMAIN" ]]; then
|
||||
@@ -117,9 +117,11 @@ elif [[ -n "$DOMAIN" ]]; then
|
||||
# Empty TLS directive — Caddy auto-manages via auto_https when the
|
||||
# site address is a hostname. Caddy also auto-creates a :80→:443
|
||||
# redirect for hostname sites, so no explicit redir block needed.
|
||||
[[ -n "$EMAIL" ]] && EXTRA_GLOBAL+=" email $EMAIL"$'\n'
|
||||
EXTRA_GLOBAL=""
|
||||
[[ -n "$EMAIL" ]] && EXTRA_GLOBAL+=" email $EMAIL"
|
||||
if [[ "$STAGING" == "1" ]]; then
|
||||
EXTRA_GLOBAL+=" acme_ca https://acme-staging-v02.api.letsencrypt.org/directory"$'\n'
|
||||
[[ -n "$EXTRA_GLOBAL" ]] && EXTRA_GLOBAL+=$'\n'
|
||||
EXTRA_GLOBAL+=" acme_ca https://acme-staging-v02.api.letsencrypt.org/directory"
|
||||
fi
|
||||
else
|
||||
echo "Caddy will issue a cert from its internal local CA (browsers will warn)."
|
||||
@@ -128,7 +130,9 @@ else
|
||||
# listener can serve any IP / hostname the device is reached on.
|
||||
# Safe without an `ask` endpoint because it's the local CA, not a
|
||||
# public one — there's no rate-limited issuer to abuse.
|
||||
TLS_DIRECTIVE=$'tls internal {\n on_demand\n }'
|
||||
TLS_DIRECTIVE="tls internal {
|
||||
on_demand
|
||||
}"
|
||||
NEEDS_REDIRECT_BLOCK=1
|
||||
fi
|
||||
|
||||
@@ -136,7 +140,7 @@ fi
|
||||
{
|
||||
echo "{"
|
||||
echo " admin off"
|
||||
[[ -n "$EXTRA_GLOBAL" ]] && printf '%s' "$EXTRA_GLOBAL"
|
||||
[[ -n "$EXTRA_GLOBAL" ]] && echo "$EXTRA_GLOBAL"
|
||||
echo "}"
|
||||
echo
|
||||
if [[ "$NEEDS_REDIRECT_BLOCK" == "1" ]]; then
|
||||
@@ -163,6 +167,13 @@ BODY
|
||||
# port 80 mapping is removed and all external traffic must enter
|
||||
# through Caddy. Inter-container traffic still reaches
|
||||
# anthias-server:8080 over the Docker network.
|
||||
#
|
||||
# FORWARDED_ALLOW_IPS=* on anthias-server is only safe BECAUSE this
|
||||
# override drops the external port mapping above — only Caddy and
|
||||
# other Docker-network containers can reach anthias-server. If you
|
||||
# re-add a host:port mapping for anthias-server, tighten this to the
|
||||
# Caddy container's IP/CIDR or back to unset, otherwise any client
|
||||
# could spoof X-Forwarded-* through to uvicorn.
|
||||
{
|
||||
cat <<EOF
|
||||
# Generated by bin/enable_ssl.sh — delete this file (and re-run
|
||||
|
||||
Reference in New Issue
Block a user