Guard Image

Image content-safety guard — closes the magic-byte / declared-MIME mismatch class and the polyglot-file class without vendoring a raster decoder. Operators bring their own decoder (sharp, jimp, libvips bindings) and feed structural metadata to the guard. KIND="metadata" — consumes ctx.metadata shape { bytes?, declaredMime?, width?, height?, frames?, colorDepth?, hasAlpha? }.

Magic-byte dispatch: inspectMagic(bytes) walks a signature table covering PNG (89 50 4E 47 0D 0A 1A 0A), JPEG (FF D8 FF), GIF87a / GIF89a, WebP (RIFF + WEBP at offset 8), BMP, ICO, TIFF (II / MM), AVIF / HEIC (ftyp boxes at offset 4), and SVG ( / ). Returns the list of distinct MIMEs that match. Multiple matches signals a polyglot file (PHP-in-JPEG / JS-in-PNG class) — refused under every profile.

Dimension caps: oversized width / height refused against maxWidth / maxHeight (strict 8 192 px, balanced 16 384 px, permissive 65 536 px). Frame caps for animated GIF / WebP / APNG / AVIF image sequences refused against maxFrames (strict 60, balanced 200, permissive 1000). Operator-supplied — the guard does not decode bytes itself; the operator's decoder reports the metadata before passing it to the gate.

Polyglot rejection: when _detectMagicMimes returns more than one distinct format, the buffer carries multiple magic-byte signatures (e.g. JPEG marker followed by an embedded ZIP central directory) — refused at every profile.

EXIF / XMP / IPTC metadata strip: sanitize removes the metadata segments in-framework by walking the container framing (JPEG APPn/COM markers, PNG ancillary text chunks, GIF comment/application extensions, WebP EXIF/XMP RIFF chunks) — the privacy-leak and metadata-stego surface, stripped without a vendored decoder. Pixel transcoding / dimension downscale still belong to the operator's decoder (sharp's withMetadata: false, libvips metadata-strip); formats whose metadata lives in an offset-based structure (TIFF / HEIC / AVIF) are refused rather than passed through.

SVG routing: bytes that match the SVG magic are refused under every profile — operators must route SVG explicitly to b.guardSvg because the SVG threat catalog (XXE, billion-laughs, animation href injection, foreignObject namespace shift) is distinct from raster threats.

Operator-feeds-metadata pattern: the gate trusts the metadata object the operator supplies. The operator's decoder is the ground truth for width / height / frames; the guard refuses based on those values. This keeps the framework's no-deps stance intact while still closing the policy gaps.

Profiles strict / balanced / permissive and compliance postures hipaa / pci-dss / gdpr / soc2 overlay on the profile baseline.

b.guardImage.validate(input, opts) #

stable0.7.13hipaapci-dssgdprsoc2
{
  profile:           "strict"|"balanced"|"permissive",
  compliancePosture: "hipaa"|"pci-dss"|"gdpr"|"soc2",
  mismatchPolicy:     "reject"|"audit"|"allow",
  polyglotPolicy:     "reject"|"audit"|"allow",
  unknownMagicPolicy: "reject"|"audit"|"allow",
  svgRoutingPolicy:   "reject"|"audit"|"allow",
  dimensionsPolicy:   "reject"|"audit"|"allow",
  framesPolicy:       "reject"|"audit"|"allow",
  maxWidth:           number,    // strict 8192, balanced 16384, permissive 65536
  maxHeight:          number,    // strict 8192, balanced 16384, permissive 65536
  maxFrames:          number,    // strict 60, balanced 200, permissive 1000
  maxBytes:           number,    // strict 32 MiB, balanced 64 MiB, permissive 256 MiB
}

Inspect an image-metadata bag { bytes?, declaredMime?, width?, height?, frames? } and return { ok, issues }. Issues carry { kind, severity, ruleId, snippet }. Detected: magic-byte / MIME mismatch (mime-mismatch), polyglot file (polyglot, refused under every profile), SVG bytes routed through guardImage (svg-routing, must go to b.guardSvg), unknown magic (unknown-magic), oversized width / height (width-cap / height-cap), excessive frame count (frames-cap), oversized byte length (image-cap). Pure inspection — never mutates input or throws on hostile metadata.

// Mismatch — declared image/png but bytes are JPEG.
var rv = b.guardImage.validate({
  bytes: Buffer.from([0xFF, 0xD8, 0xFF]),
  declaredMime: "image/png",
}, { profile: "strict" });
rv.ok;                                               // → false
rv.issues[0].kind;                                   // → "mime-mismatch"

// Oversized width refused under strict (8192 px cap).
var big = b.guardImage.validate({
  bytes: Buffer.from([0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A]),
  declaredMime: "image/png",
  width: 16384, height: 16384,
}, { profile: "strict" });
big.issues.some(function (i) { return i.kind === "width-cap"; });
//                                                   → true

b.guardImage.sanitize(input, opts) #

stable0.7.13
{
  profile:           "strict"|"balanced"|"permissive",
  compliancePosture: "hipaa"|"pci-dss"|"gdpr"|"soc2",
}

Strip the container's metadata segments from input.bytes — EXIF/GPS, XMP/IPTC, and comment payloads (the privacy-leak and metadata-stego surface) — and return the bag with the cleaned bytes. Stripping walks the linear container framing (JPEG APPn/COM markers, PNG ancillary text chunks, GIF comment/application extensions, WebP EXIF/XMP RIFF chunks); pixel transcoding and dimension downscale still need a vendored decoder and stay the operator's job.

sanitize first runs the validate chain and re-throws GuardImageError when any issue is critical or high (a polyglot or MIME-mismatch is refused, never stripped). A format whose metadata lives in an offset-based structure that cannot be rewritten without a decoder (TIFF / HEIC / AVIF) is refused with image.sanitize-unsupported-format; a structurally malformed container is refused with image.sanitize-malformed rather than returned half-stripped. BMP / ICO carry no metadata container and pass through.

// EXIF-laden JPEG → the APP1 (EXIF/XMP) segment is removed.
var clean = b.guardImage.sanitize({
  bytes: jpegWithExif,
  declaredMime: "image/jpeg",
}, { profile: "strict" });
clean.bytes.length < jpegWithExif.length;            // → true

// A MIME-mismatch is refused, not stripped.
try {
  b.guardImage.sanitize({
    bytes: Buffer.from([0xFF, 0xD8, 0xFF]),
    declaredMime: "image/png",
  }, { profile: "strict" });
} catch (e) {
  e.code;                                            // → "image.mime-mismatch"
}

b.guardImage.gate(opts) #

stable0.7.13hipaapci-dssgdprsoc2
{
  profile:    "strict"|"balanced"|"permissive",
  compliancePosture: "hipaa"|"pci-dss"|"gdpr"|"soc2",
  name:       string,
  ...:        any validate opt
}

Build a b.gateContract gate suitable for b.fileUpload({ contentSafety: { "image/png": gate, "image/jpeg": gate } }) or b.staticServe. Operators pass ctx.metadata (the decoder's reported shape) plus the original bytes. Action chain: serve (no issues) → audit-only (warn-only) → refuse (any critical / high). The gate does not auto-strip; an operator who wants metadata removed before serving calls b.guardImage.sanitize(bag) explicitly (it walks the container framing — EXIF/XMP/IPTC out of JPEG/PNG/GIF/WebP).

var imgGate = b.guardImage.gate({ profile: "strict" });

var verdict = await imgGate.check({
  metadata: {
    bytes: Buffer.from([0xFF, 0xD8, 0xFF]),
    declaredMime: "image/png",
    width: 1024, height: 768, frames: 1,
  },
});
verdict.action;                                      // → "refuse"
verdict.issues[0].kind;                              // → "mime-mismatch"

b.guardImage.inspectMagic(bytes) #

stable0.7.13

Read the leading bytes of bytes and return an array of distinct MIMEs that match a known image-format magic-byte signature. Empty array on no match; multiple entries signals a polyglot file. Pure inspection — never mutates input or throws.

var pngBytes = Buffer.from([0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A]);
b.guardImage.inspectMagic(pngBytes);                 // → ["image/png"]

b.guardImage.inspectMagic(Buffer.from([0xFF, 0xD8, 0xFF]));
//                                                   → ["image/jpeg"]

b.guardImage.compliancePosture(name) #

stable0.7.13hipaapci-dssgdprsoc2

Look up a compliance-posture overlay by name (one of "hipaa" / "pci-dss" / "gdpr" / "soc2"). Returns a fresh clone of the posture overlay so the caller may mutate it freely without disturbing the shared table. Throws GuardImageError with code "image.bad-posture" when the name is not one this guard maps. Wired by gateContract.defineGuard through gateContract.lookupCompliancePosture, so the clone semantics and error code are identical across every guard in the family.

var posture = b.guardImage.compliancePosture("hipaa");
posture;                                             // → overlay clone (mutable)

try {
  b.guardImage.compliancePosture("not-a-regime");
} catch (e) {
  e.code;                                            // → "image.bad-posture"
}

b.guardImage.buildProfile(opts) #

stable0.7.13
{
  extends:   string|string[],   // base profile name(s) to compose
  ...:       any guard key,      // inline override of resolved keys
}

Compose a derived profile from one or more named bases plus inline overrides, resolving names through this guard's own PROFILES table. opts.extends is a base profile name ("strict" / "balanced" / "permissive") or an array of names — later entries shadow earlier ones, and inline opts keys win last. Wired by gateContract.defineGuard through gateContract.makeProfileBuilder, so operator-defined profiles stay traceable to a baseline instead of a hand-typed dictionary.

var custom = b.guardImage.buildProfile({ extends: "strict" });
custom;                                              // → composed profile object

b.guardImage.loadRulePack(pack) #

stable0.7.13

Register an operator-supplied rule pack with this guard's rule-pack registry. The pack is identified by pack.id (a non-empty string) and stored for later dispatch by gates that opt in via opts.rulePackId. Returns the pack unchanged on success; throws GuardImageError with code "image.bad-opt" when pack is missing or pack.id is not a non-empty string. Wired by gateContract.defineGuard through gateContract.makeRulePackLoader, so storage shape and validation are identical across the family.

var pack = b.guardImage.loadRulePack({ id: "tenant-policy", rules: [] });
pack.id;                                             // → "tenant-policy"

b.guardImage.resolveOpts(opts?) #

stable0.7.13
{
  profile:           string,    // one of PROFILES; default this guard's default
  compliancePosture: string,    // overlay one of hipaa/pci-dss/gdpr/soc2
}

Resolve caller opts against this guard's PROFILES + compliance-posture overlays into the fully-defaulted option set the guard runs on — the same resolution validate / sanitize / gate apply internally. Wired by gateContract.defineGuard from the guard's binding config (profiles / postures / defaults / error class), so a guard's bespoke gate calls resolveOpts instead of re-declaring the per-guard resolver wrapper. Throws GuardImageError with code "image.bad-opt" / "image.bad-posture" on an unknown profile or posture name.

var resolved = b.guardImage.resolveOpts({ profile: "strict" });
resolved.profile;                                    // → "strict"

Last updated 2026-08-08T16:39:49.652Z by seeder.