Skip to main content

Security

In production, always set IMAGOR_SECRET to require URL signatures on every request. IMAGOR_UNSAFE bypasses signature verification and should only be used during development.

URL Signature​

Set IMAGOR_SECRET to require every request URL to carry a valid HMAC signature. This prevents DDoS attacks that abuse arbitrary image operations and stops unauthenticated use of your imagor instance. Do not use IMAGOR_UNSAFE in production — it disables signature verification entirely.

The signature hash is computed from the URL path (excluding /unsafe/) using the secret, then Base64 URL encoded and prepended to the path:

import hmac
import hashlib
import base64

def sign(path: str, secret: str) -> str:
h = hmac.new(secret.encode(), path.encode(), hashlib.sha1)
hash = base64.urlsafe_b64encode(h.digest()).decode()
return hash + '/' + path

print(sign('500x500/top/raw.githubusercontent.com/cshum/imagor/master/testdata/gopher.png', 'mysecret'))
# cST4Ko5_FqwT3BDn-Wf4gO3RFSk=/500x500/top/raw.githubusercontent.com/cshum/imagor/master/testdata/gopher.png

Custom HMAC Signer​

imagor uses SHA1 HMAC by default, the same algorithm used by thumbor. SHA1 is not considered cryptographically secure today. Use sha256 or sha512 and optionally truncate the hash length:

IMAGOR_SIGNER_TYPE=sha256
IMAGOR_SIGNER_TRUNCATE=40

The signing function then becomes:

import hmac
import hashlib
import base64

def sign(path: str, secret: str) -> str:
h = hmac.new(secret.encode(), path.encode(), hashlib.sha256)
hash = base64.urlsafe_b64encode(h.digest()).decode()[:40]
return hash + '/' + path

print(sign('500x500/top/raw.githubusercontent.com/cshum/imagor/master/testdata/gopher.png', 'mysecret'))
# IGEn3TxngivD0jy4uuiZim2bdUCvhcnVi1Nm0xGy/500x500/top/raw.githubusercontent.com/cshum/imagor/master/testdata/gopher.png

URL Expiry​

Use the expire filter to give a signed URL a hard expiry time. The timestamp is unix milliseconds.

import time

# URL expires in 5 minutes
expiry = int(time.time() * 1000) + 5 * 60 * 1000
path = f'500x500/filters:expire({expiry})/raw.githubusercontent.com/cshum/imagor/master/testdata/gopher.png'
print(sign(path, 'mysecret'))

imagor rejects requests whose expire timestamp is in the past, even if the signature is otherwise valid.


Allowed Sources​

Restricting which hosts the HTTP Loader can fetch from is an important measure against SSRF and open-proxy abuse.

Glob allowlist — comma-separated glob patterns:

HTTP_LOADER_ALLOWED_SOURCES=*.mydomain.com,assets.cdn.io,s3.amazonaws.com

Regex allowlist — full regular expression:

HTTP_LOADER_ALLOWED_SOURCE_REGEXP=^([\w-]+\.)?mydomain\.com$

Base URL lock — restrict loading to a single origin and shorten image URLs:

HTTP_LOADER_BASE_URL=https://assets.mydomain.com/

With a base URL set the image path is appended to it, so 500x500/photo.jpg loads https://assets.mydomain.com/photo.jpg.

HTTPS only — refuse plain-HTTP image sources:

HTTP_LOADER_HTTPS_ONLY=1

See HTTP Loader for the full configuration reference.


Image Bombs Prevention​

imagor checks the image type and resolution before processing begins. Requests are rejected when image dimensions exceed configured limits, protecting against "image bomb" attacks:

VIPS_MAX_RESOLUTION=16800000
VIPS_MAX_WIDTH=5000
VIPS_MAX_HEIGHT=5000

Timeouts and Concurrency​

Limit per-request budget and parallel processing capacity to defend against slowloris and resource exhaustion:

IMAGOR_REQUEST_TIMEOUT=30s          # total request budget
IMAGOR_LOAD_TIMEOUT=10s # loader fetch must complete within this time
IMAGOR_PROCESS_CONCURRENCY=20 # max simultaneous image processing jobs
IMAGOR_PROCESS_QUEUE_SIZE=100 # max queued jobs before 429 is returned

SVG Pass-through​

imagor renders SVG as an image by default: JPEG, WebP with IMAGOR_AUTO_WEBP=1, AVIF with IMAGOR_AUTO_AVIF=1, or whatever a format() filter asks for. Serving the SVG itself means any script in it runs from imagor's origin, so imagor sanitizes the SVG document before serving it.

Both IMAGOR_PASSTHROUGH_FORMATS=svg and format(svg) return that document, sanitized.

IMAGOR_PASSTHROUGH_FORMATS=svg

SVG Sanitization​

imagor sanitizes the SVG document before serving it, for pass-through and format(svg). It inspects and rewrites the document.

  • An allowlist of elements and attributes. Everything else is dropped, including <script>, event handlers such as onload, <foreignObject>, animation, DOCTYPE, comments and processing instructions.
  • References. url(#id) and xlink:href="#id" resolve inside the document. A reference that leaves it is dropped.
  • Links. An absolute http(s) <a href> is kept: it only goes somewhere when a reader clicks. A relative link is dropped, since it would resolve against imagor's host.
  • <image>. An embedded data: raster is kept. A nested SVG is dropped, because the allowlist cannot look inside it.
  • Re-serialized. Whitespace, comments and namespace prefixes are lost.
  • UTF-8 output, whatever the source declared. UTF-8, ASCII and ISO-8859-1 are the charsets read.
  • Rendered as an image instead when a document declares another charset, fails to parse, carries CSS in a <style> element, or is larger than 32 MB. A format(svg) request answers with an error.

IMAGOR_SANITIZE_SVG=0 turns sanitization off and serves the document byte for byte. Set it only when every source SVG is trusted.

Response headers​

Any response with content type image/svg+xml carries this policy, whether it comes from pass-through, raw() or the result cache:

Content-Type: image/svg+xml
Content-Security-Policy: default-src 'none'; style-src 'unsafe-inline'; img-src data:; sandbox
X-Content-Type-Options: nosniff

default-src 'none' blocks CSS and image loads, which script-src 'none' alone would not. sandbox gives a document served with sanitization off its own origin. nosniff stops the browser from reading the bytes as another type.

warning

Turning this on does not make an untrusted SVG safe. If you do not control the sources, leave IMAGOR_PASSTHROUGH_FORMATS unset and keep rendering the SVG as an image.


Production Setup​

A minimal production setup bringing together the key configurations above:

version: "3"
services:
imagor:
image: shumc/imagor:latest
environment:
PORT: 8000

IMAGOR_SECRET: your-secret-here # required — signs all URLs
IMAGOR_SIGNER_TYPE: sha256 # stronger hash than the default SHA1
IMAGOR_SIGNER_TRUNCATE: 40 # truncate to 40 chars

HTTP_LOADER_ALLOWED_SOURCES: "*.mydomain.com,assets.cdn.io" # restrict sources
HTTP_LOADER_HTTPS_ONLY: 1 # refuse plain-HTTP image URLs

VIPS_MAX_RESOLUTION: 16800000 # reject images over ~4K×4K
VIPS_MAX_WIDTH: 5000
VIPS_MAX_HEIGHT: 5000

IMAGOR_REQUEST_TIMEOUT: 30s
IMAGOR_LOAD_TIMEOUT: 10s
IMAGOR_PROCESS_CONCURRENCY: 20 # cap simultaneous processing
ports:
- "8000:8000"