diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml index 2631d1f..85e03ea 100644 --- a/.github/workflows/pr.yml +++ b/.github/workflows/pr.yml @@ -180,7 +180,11 @@ jobs: origin=$(docker inspect -f \ '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' dims-origin) docker rm -f dims-ci - docker run -d --name dims-ci -e DIMS_WHITELIST="$origin" mod-dims:ci + # The origin runs on a private network address, so this run opts in. + docker run -d --name dims-ci \ + -e DIMS_WHITELIST="$origin" \ + -e DIMS_ALLOW_PRIVATE_ADDRESSES=on \ + mod-dims:ci sleep 5 docker exec dims-ci curl -fsS -o /tmp/out.png \ "http://127.0.0.1:8000/dims3/development/resize/100x100/?url=http://$origin/grid.png" diff --git a/conf/mod_dims.conf.example b/conf/mod_dims.conf.example index bfe3e37..b5f9fef 100644 --- a/conf/mod_dims.conf.example +++ b/conf/mod_dims.conf.example @@ -5,6 +5,9 @@ # # Installing the module does not edit httpd.conf. That is deliberate: a build # step must not change a running server's configuration. +# +# The settings below are the secure ones. Each note says what it protects and +# when to relax it. LoadModule dims_module modules/libmod_dims.so @@ -14,10 +17,41 @@ DimsDownloadTimeout 60000 DimsImagemagickTimeout 20000 +# The largest source image to accept, in bytes. A larger source is refused +# before it is decoded. This bounds memory and a malicious SVG. 0 means no +# limit. +DimsMaxSourceBytes 33554432 + +# Refuse a fetch to a private or loopback address, so a request cannot reach an +# internal service. Set it to On only when your origin is on a private network. +# The cloud metadata address is refused whatever this holds. +DimsAllowPrivateAddresses off + +# The hosts an image may come from. A leading *. matches subdomains only, not +# the apex. This list applies to /dims3/. A signed request skips it unless you +# enable DimsAllowlistSigned below. +DimsAddWhitelist images.example.com +DimsAddWhitelist *.cdn.example.com + +# Apply the host allowlist to a signed request and to a redirect. A signed +# request skips the allowlist by default. Enable this once the allowlist covers +# your origins, so a signed request is held to the same list. Set it to log +# first to record what enforcing would refuse. +# DimsAllowlistSigned enforce + +# /dims5/ is the endpoint to use for a new integration. It signs with +# HMAC-SHA256. Set a long random key and keep it out of version control. +DimsSigningKey CHANGE_ME_TO_A_LONG_RANDOM_KEY + # DimsAddClient # -# The secret is the key for /dims4/ signatures. Keep it out of version control. -DimsAddClient CLIENT http://example.com/no-image.png 604800 604800 trust 300 86400 changeme +# The secret is the key for /dims4/ signatures. Use a long random secret and +# keep it out of version control. Prefer /dims5/ for a new integration. +DimsAddClient CLIENT http://example.com/no-image.png 604800 604800 trust 300 86400 CHANGE_ME_TO_A_LONG_RANDOM_SECRET + +# Decrypt the eurl parameter with AES-GCM, which is authenticated. The client +# that builds an eurl must encrypt with the same. +DimsEncryptionAlgorithm AES/GCM/NoPadding # Shown when a source image cannot be fetched or processed. DimsDefaultImageURL http://example.com/no-image.png @@ -25,11 +59,13 @@ DimsDefaultImageURL http://example.com/no-image.png DimsCacheExpire 604800 DimsNoImageCacheExpire 60 -# Hosts that /dims3/ may fetch from. A leading *. matches subdomains only, not -# the apex. Signed /dims4/ requests skip this list entirely, which -# specs/code-review.md records as finding C1. -DimsAddWhitelist images.example.com -DimsAddWhitelist *.cdn.example.com +# The status page reports counters. Off drops the component versions, which name +# the libraries a caller would target. +DimsStatusVerbose off + + + SetHandler dims5 + SetHandler dims3 @@ -39,6 +75,10 @@ DimsAddWhitelist *.cdn.example.com SetHandler dims4 +# Restrict the status page to your monitoring host. Uncomment a Require line +# once mod_authz_host is loaded. SetHandler dims-status + # Require local + # Require ip 10.0.0.0/8 diff --git a/docker/Dockerfile b/docker/Dockerfile index 2206446..3e3bfd3 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -50,6 +50,11 @@ ENV DIMS_SIGNING_KEY="-" ENV DIMS_WHITELIST="" ENV DIMS_CACHE_EXPIRE=604800 ENV DIMS_NO_IMAGE_CACHE_EXPIRE=60 +ENV DIMS_MAX_SOURCE_BYTES=33554432 +ENV DIMS_ALLOWLIST_SIGNED=log +ENV DIMS_ALLOW_PRIVATE_ADDRESSES=off +ENV DIMS_STATUS_VERBOSE=off +ENV DIMS_ENCRYPTION_ALGORITHM=AES/GCM/NoPadding ENV LC_ALL="C" # The image libraries are not installed here. The builder compiles each one diff --git a/docker/dims.conf b/docker/dims.conf index 05431a2..d12a1f1 100644 --- a/docker/dims.conf +++ b/docker/dims.conf @@ -11,6 +11,23 @@ CustomLog /proc/self/fd/1 common DimsDownloadTimeout ${DIMS_DOWNLOAD_TIMEOUT} DimsImagemagickTimeout ${DIMS_IMAGEMAGICK_TIMEOUT} +# The largest source image to accept, in bytes. A larger source is refused +# before it is decoded. 0 means no limit. +DimsMaxSourceBytes ${DIMS_MAX_SOURCE_BYTES} + +# Apply the host allowlist to a signed request and to a redirect. The default +# is log, so a signed request reaches any host. Set DIMS_ALLOWLIST_SIGNED to +# enforce to hold a signed request to the allowlist, once DIMS_WHITELIST covers +# your origins. +DimsAllowlistSigned ${DIMS_ALLOWLIST_SIGNED} + +# Refuse a fetch to a private or loopback address. The default is off. Set +# DIMS_ALLOW_PRIVATE_ADDRESSES to On when the origin is on a private network. +DimsAllowPrivateAddresses ${DIMS_ALLOW_PRIVATE_ADDRESSES} + +# The status page reports counters. The default drops the component versions. +DimsStatusVerbose ${DIMS_STATUS_VERBOSE} + # DimsClient [ ] DimsAddClient ${DIMS_CLIENT} ${DIMS_NO_IMAGE_URL} ${DIMS_CACHE_CONTROL_MAX_AGE} ${DIMS_EDGE_CONTROL_DOWNSTREAM_TTL} ${DIMS_TRUST_SOURCE} ${DIMS_MIN_SOURCE_CACHE} ${DIMS_MAX_SOURCE_CACHE} ${DIMS_SECRET} @@ -19,11 +36,17 @@ DimsCacheExpire ${DIMS_CACHE_EXPIRE} DimsNoImageCacheExpire ${DIMS_NO_IMAGE_CACHE_EXPIRE} DimsUserAgentEnabled true +# Decrypt the eurl parameter with AES-GCM, which is authenticated. The default +# is AES/GCM/NoPadding. Set DIMS_ENCRYPTION_ALGORITHM to AES/ECB/PKCS5Padding +# only for a client that still encrypts with it. +DimsEncryptionAlgorithm ${DIMS_ENCRYPTION_ALGORITHM} + # A single dash means no key, and every /dims5/ request is then refused. DimsSigningKey ${DIMS_SIGNING_KEY} -# The allowlist decides which hosts /dims3/ may fetch from. DIMS_WHITELIST -# defaults to empty in the Dockerfile, and an empty allowlist matches nothing. +# The allowlist decides which hosts a request may fetch from. DIMS_WHITELIST +# defaults to empty in the Dockerfile, and an empty allowlist matches nothing, +# so set it before the service can fetch a source. DimsAddWhitelist ${DIMS_WHITELIST} ## Handler definitions. ## @@ -42,4 +65,4 @@ DimsAddWhitelist ${DIMS_WHITELIST} SetHandler dims-status - \ No newline at end of file + diff --git a/docs/docs/configuration/index.md b/docs/docs/configuration/index.md index 4642593..11fc19b 100644 --- a/docs/docs/configuration/index.md +++ b/docs/docs/configuration/index.md @@ -24,6 +24,29 @@ DimsSigningKey a-long-random-string ``` +## Hardening + +The shipped `conf/mod_dims.conf.example` sets these. Each is safe to relax when +your deployment needs it. + +- `DimsMaxSourceBytes` caps the source size, so one request cannot exhaust + memory. A malicious SVG is bounded by this too. +- `DimsAllowPrivateAddresses off` refuses a fetch to an internal address. The + cloud metadata address is refused whatever this holds. +- `DimsStatusVerbose off` drops the component versions from the status page, so + it does not name the libraries a caller would target. Restrict the + `/dims-status/` location to your monitoring host as well. +- `DimsEncryptionAlgorithm AES/GCM/NoPadding` decrypts the `eurl` parameter with + an authenticated cipher. The client that builds an `eurl` must encrypt with + the same. + +Two more are worth enabling as your deployment allows. + +- `DimsAllowlistSigned enforce` holds a signed request to the allowlist. A + signed request skips it by default. Enable it once the allowlist covers your + origins. +- Prefer `/dims5/` for a new integration. It signs with HMAC-SHA256. + ## Every directive | Directive | Default | diff --git a/docs/docs/endpoints/dims4.md b/docs/docs/endpoints/dims4.md index 52767b0..d95d5de 100644 --- a/docs/docs/endpoints/dims4.md +++ b/docs/docs/endpoints/dims4.md @@ -145,15 +145,12 @@ expiry may be, which stops a caller minting a URL that never expires. ## Unsigned parameters -`overlay` and `optimizeResize` are only covered when `_keys` lists them. A -caller who omits `_keys` can change either and keep a valid signature. - -:::warning -Six hexadecimal characters is 24 bits. One in 16.8 million random signatures -is accepted, so a caller sending requests at a moderate rate finds a working -signature for a URL of their choosing in hours. +`overlay` and `optimizeResize` are covered only when `_keys` lists them. List +them in `_keys` so the signature covers them. +:::tip Set [`DimsAllowlistSigned`](/configuration/image-sources) to `enforce` so the -host allowlist applies to a signed request too. That bounds what a forged -signature can reach. +host allowlist applies to a signed request too. Prefer +[`/dims5/`](/endpoints/dims5), which signs with HMAC-SHA256, for a new +integration. ::: diff --git a/docs/docs/endpoints/index.md b/docs/docs/endpoints/index.md index 5233a91..7523a3e 100644 --- a/docs/docs/endpoints/index.md +++ b/docs/docs/endpoints/index.md @@ -11,8 +11,7 @@ Four handlers, each set on a location. | [`dims-local`](/endpoints/local) | a file on disk | nothing | `/dims5/` is the one to use. Its signature covers the commands, the image URL, -and every query parameter apart from five, and it compares a full HMAC-SHA256 -rather than the first six characters of an MD5. +and every query parameter apart from five, and it uses HMAC-SHA256. ## Commands