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:
- Python
- Node.js
- PHP
- Go
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
const crypto = require('crypto');
function sign(path, secret) {
const hash = crypto.createHmac('sha1', secret)
.update(path)
.digest('base64')
.replace(/\+/g, '-').replace(/\//g, '_')
return hash + '/' + path
}
console.log(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
<?php
function sign(string $path, string $secret): string {
$hash = base64_encode(hash_hmac('sha1', $path, $secret, true));
$hash = strtr($hash, '+/', '-_');
return $hash . '/' . $path;
}
echo 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
import (
"crypto/hmac"
"crypto/sha1"
"encoding/base64"
)
func sign(path, secret string) string {
mac := hmac.New(sha1.New, []byte(secret))
mac.Write([]byte(path))
return base64.URLEncoding.EncodeToString(mac.Sum(nil)) + "/" + path
}
// 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:
- Python
- Node.js
- PHP
- Go
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
const crypto = require('crypto');
function sign(path, secret) {
const hash = crypto.createHmac('sha256', secret)
.update(path)
.digest('base64')
.slice(0, 40)
.replace(/\+/g, '-').replace(/\//g, '_')
return hash + '/' + path
}
console.log(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
<?php
function sign(string $path, string $secret): string {
$hash = base64_encode(hash_hmac('sha256', $path, $secret, true));
$hash = strtr(substr($hash, 0, 40), '+/', '-_');
return $hash . '/' . $path;
}
echo 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
import (
"crypto/hmac"
"crypto/sha256"
"encoding/base64"
)
func sign(path, secret string) string {
mac := hmac.New(sha256.New, []byte(secret))
mac.Write([]byte(path))
return base64.URLEncoding.EncodeToString(mac.Sum(nil))[:40] + "/" + path
}
// 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.
- Python
- Node.js
- PHP
- Go
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'))
// URL expires in 5 minutes
const expiry = Date.now() + 5 * 60 * 1000;
const path = `500x500/filters:expire(${expiry})/raw.githubusercontent.com/cshum/imagor/master/testdata/gopher.png`;
console.log(sign(path, 'mysecret'));
// URL expires in 5 minutes
$expiry = (int)(microtime(true) * 1000) + 5 * 60 * 1000;
$path = "500x500/filters:expire({$expiry})/raw.githubusercontent.com/cshum/imagor/master/testdata/gopher.png";
echo sign($path, 'mysecret');
import (
"fmt"
"time"
)
// URL expires in 5 minutes
expiry := time.Now().UnixMilli() + 5*60*1000
path := fmt.Sprintf("500x500/filters:expire(%d)/raw.githubusercontent.com/cshum/imagor/master/testdata/gopher.png", expiry)
fmt.Println(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
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"