URL Structure & Signing
Every transformation lives in the request path, and the path is authenticated with an HMAC signature. The layout mirrors imgproxy's. The source URL is given either in plain text behind a plain/ prefix, or Base64-encoded.
Path anatomy
http(s)://<host>/<signature>/<processing_options>/plain/<percent-encoded-source>@<extension>
http(s)://<host>/<signature>/<processing_options>/<base64url-source>.<extension>A worked example, with the output format declared by @webp:
https://imgforge.example.com/Q7j8KNpM/resize:fill:800:600/quality:85/plain/https%3A%2F%2Fexample.com%2Fcat.jpg@webp
└─ signature
└─ processing options, slash-separated
└─ source: plain/ + percent-encoded URL
└─ output format(A real signature is 43 characters — a Base64 URL-safe, unpadded SHA-256 HMAC.)
| Segment | Description |
|---|---|
<signature> | Base64 URL-safe, unpadded HMAC-SHA256 digest generated from the path. Use unsafe when unsigned URLs are permitted. |
<processing_options> | Slash-separated list of directives (e.g., resize:fill:800:600/quality:85). See Processing Options. |
plain/... | Indicates the source URL is provided in plain text (percent-encoded if needed) and may include @<extension> to declare the output format. |
<base64url-source> | The source URL encoded using URL-safe Base64 without padding (=). The output extension, if specified, is appended after a dot. |
Choosing between plain and Base64
Source URLs can be provided in two formats:
- Plain format (
plain/prefix): Use when the source URL contains only URL-safe characters and you want to specify the output format explicitly with@<extension> - Base64 format: Use when the source URL contains special characters, query parameters, or when you want to avoid potential encoding conflicts
Examples
Plain URL format with format conversion:
/<sig>/resize:fit:1024:0/plain/https://example.com/cats/siamese.jpg@webpBase64-encoded URL format:
/<sig>/resize:fill:800:600/aHR0cHM6Ly9leGFtcGxlLmNvbS9jYXRzL3NpYW1lc2UuanBn.jpgNote: aHR0cHM6Ly9leGFtcGxlLmNvbS9jYXRzL3NpYW1lc2UuanBn is the Base64URL encoding of https://example.com/cats/siamese.jpg
Plain URL with query parameters (requires Base64):
/<sig>/resize:fit:800:0/aHR0cHM6Ly9leGFtcGxlLmNvbS9pbWFnZS5qcGc_dj0xMjM.webpNote: aHR0cHM6Ly9leGFtcGxlLmNvbS9pbWFnZS5qcGc_dj0xMjM is the Base64URL encoding of https://example.com/image.jpg?v=123
Generate Base64 URL-safe strings without padding (replace + with -, / with _, and remove trailing =).
Signing a URL
Why signatures matter
Signed URLs prevent tampering. Anyone with write access to a CDN, cache, or browser can attempt to modify processing directives to produce oversized images or trigger expensive transformations. The HMAC signature ensures only parties who know IMGFORGE_KEY and IMGFORGE_SALT can generate valid requests.
How signing works
- Convert
IMGFORGE_KEYandIMGFORGE_SALTfrom hex to raw bytes. - Build the path portion beginning with the slash before the processing options (for example
/resize:fill:800:600/plain/...). - Concatenate the salt bytes with the path bytes.
- Compute an HMAC-SHA256 digest using the key from step 1.
- Encode the digest using Base64 URL-safe without padding.
- Prefix the signature to the path and send the request.
The same steps apply for both plain and Base64 source segments. Rotate keys periodically and store them securely; leaking either value lets attackers craft arbitrary URLs.
Interactive demo
Use the form below to experiment with different keys, salts, and paths. The output matches the signature algorithm that imgforge expects.
Language examples
Rust
use base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine as _};
use hmac::{Hmac, Mac};
use sha2::Sha256;
type HmacSha256 = Hmac<Sha256>;
let key = hex::decode(std::env::var("IMGFORGE_KEY").unwrap()).unwrap();
let salt = hex::decode(std::env::var("IMGFORGE_SALT").unwrap()).unwrap();
let path = "/resize:fill:800:600/plain/https://example.com/cat.jpg@webp";
let mut mac = HmacSha256::new_from_slice(&key).unwrap();
mac.update(&salt);
mac.update(path.as_bytes());
let signature = URL_SAFE_NO_PAD.encode(mac.finalize().into_bytes());
println!("{}{}", signature, path);Python
import base64, hmac, hashlib, os
key = bytes.fromhex(os.environ["IMGFORGE_KEY"])
salt = bytes.fromhex(os.environ["IMGFORGE_SALT"])
path = "/resize:fill:800:600/plain/https://example.com/cat.jpg@webp"
digest = hmac.new(key, salt + path.encode(), hashlib.sha256).digest()
signature = base64.urlsafe_b64encode(digest).rstrip(b"=").decode()
print(f"{signature}{path}")Keep the signing logic in one shared helper. When application servers, static-site generators, and edge functions each grow their own copy, they drift, and the failure looks like a 403 rather than a bug.
Unsigned URLs (unsafe)
When IMGFORGE_ALLOW_UNSIGNED=true, the signature segment can be replaced with unsafe:
http://localhost:3000/unsafe/resize:fit:600:0/plain/https://example.com/dog.jpgUse this mode for development only; it bypasses HMAC validation entirely.
Common signing mistakes
- Incorrect path prefix: Include the leading slash (
/resize:...) when computing the digest. - Hex decoding:
IMGFORGE_KEYandIMGFORGE_SALTmust decode to raw bytes. Do not reuse the hex string directly. - Padding: Remove trailing
=when encoding the signature using Base64 URL-safe. - Salt omission: Always concatenate the salt bytes before the path.
- URL normalization: Ensure the source URL is percent-encoded identically in both the signature computation and the request.
Next steps
- Processing Options – what you can put in the options segment.
- Presets – name a chain of options once and reference it by name.