TL;DR: Strip embedded metadata from every public smart-cropped derivative. If photographer attribution has a legitimate product purpose, store a reviewed credit as an explicit database field and render it beside the image. Do not make viewers download EXIF, IPTC, or XMP data to discover the credit.
For a fintech upload flow, the practical split is upload-time sanitation plus on-demand crops. Upload time is the only dependable privacy boundary: decode the source, apply its orientation, create a metadata-free canonical image, and quarantine or delete the original according to the retention policy. Generate 1:1, 4:3, and 16:9 variants when traffic justifies the storage; otherwise derive them from that clean canonical image and cache them. This keeps private source bytes out of the public delivery path without forcing the team to predict every future aspect ratio.
That distinction matters. Metadata can carry camera details, timestamps, descriptive fields, and location information. Attribution is useful, but an opaque payload is a poor consent mechanism. A visible, editable credit has clearer ownership and can survive a format conversion.
Should You Strip Image Metadata or Keep Photographer Attribution?
Start with a field-level policy, not a “keep metadata” checkbox. A receipt or card-support image usually needs pixels, an owner identifier, upload time, processing state, and perhaps a user-entered caption. It does not need the source file's GPS coordinates or device identity in a public thumbnail.
Photographer attribution is different when the image is licensed editorial material, such as a market report cover. Keep the minimum reviewed fields in application data: display name, credit line, license reference, and consent or provenance record. The public image still leaves the pipeline without embedded metadata. The page template prints the credit, while authorization controls who may inspect the provenance record.
No exceptions.
Preserve meaning, not the original container. Copying every metadata block because one IPTC field might be valuable also copies fields nobody reviewed. Deleting everything without first reading orientation can rotate the visible result. The safe order is inspect, normalize, crop, encode, verify.
A small Node.js upload boundary
The example below accepts an already-authorized local upload path. It reads dimensions and orientation for validation, uses the orientation while normalizing pixels, writes a clean canonical image, and creates deterministic crops. The code intentionally does not call withMetadata(), because Sharp removes metadata from output by default. Production code still needs file-signature validation, upload size limits, isolated temporary storage, and authorization before this function runs.
import { mkdir } from "node:fs/promises";
import { join } from "node:path";
import sharp from "sharp";
type Ratio = Readonly<{ name: string; width: number; height: number }>;
const ratios: readonly Ratio[] = [
{ name: "square", width: 1200, height: 1200 },
{ name: "statement", width: 1600, height: 1200 },
{ name: "wide", width: 1600, height: 900 },
];
export async function createPublicCrops(
sourcePath: string,
outputDirectory: string,
assetId: string,
): Promise<string[]> {
const source = sharp(sourcePath, { failOn: "error" });
const metadata = await source.metadata();
if (!metadata.width || !metadata.height || metadata.pages !== undefined && metadata.pages > 1) {
throw new Error("Expected a single-frame raster image with known dimensions");
}
await mkdir(outputDirectory, { recursive: true });
const canonicalPath = join(outputDirectory, `${assetId}-canonical.webp`);
await sharp(sourcePath, { failOn: "error" })
.rotate()
.webp({ quality: 88 })
.toFile(canonicalPath);
return Promise.all(
ratios.map(async ({ name, width, height }) => {
const destination = join(outputDirectory, `${assetId}-${name}.webp`);
await sharp(canonicalPath)
.resize(width, height, { fit: "cover", position: "attention" })
.webp({ quality: 82 })
.toFile(destination);
return destination;
}),
);
}
The crop position above is content-aware, not finance-aware. A card number, signature, or total can still land inside a crop. Smart cropping is a composition tool; it is not redaction. Sensitive-document classification and redaction belong before publication, with a blocked state when confidence or policy says the asset needs review.
One subtle failure deserves attention: reading orientation after the first transform is too late. Rotation changes the effective width and height. Normalize once into the canonical image, then make every aspect ratio from the same pixels. This also prevents two workers from interpreting the source differently.
Upload time or on demand?
Use upload time for irreversible safety work and on-demand processing for reversible presentation choices. Metadata removal, orientation normalization, decode validation, policy checks, and creation of the clean canonical image belong before an asset can become public. Aspect ratios are presentation choices, so they can be eager, lazy, or mixed.
That is the boundary.
| Decision | At upload | On demand |
|---|---|---|
| Privacy boundary | Strong: public jobs never need source bytes | Weak if the request path can reach the original |
| First request | Predictable for prebuilt ratios | Pays crop and encode latency on a cache miss |
| Storage | Grows with every prebuilt variant | Grows only with requested variants |
| New layouts | Requires a backfill for eager-only pipelines | Can create a new ratio from the canonical image |
| Failure handling | Upload stays pending until required work passes | Viewer requests can encounter processing failures |
For a solo-operated service, a hybrid is usually easier to reason about: build the canonical image and the two dominant ratios during ingestion, then produce uncommon ratios from the canonical image behind a bounded job queue. Never regenerate from the private original merely to change presentation. The canonical image is the publication boundary.
This design has a real limitation. It is not suitable when an evidentiary, archival, or contractual workflow requires delivery of the original file with its metadata intact. Put those originals in a separate, access-controlled evidence path and do not pass them through the public crop system. Eager generation also trades extra storage and upload latency for predictable reads, while lazy generation trades a slower first request and more moving parts for fewer unused files. There is no universal winner: a fixed mobile feed can justify eager crops, but a report builder with user-defined frames is a better fit for on-demand variants from the sanitized canonical image.
Do not let cost accounting hide the latency trade. Track decoded pixel count, encode duration, output bytes, cache hits, and job retries per ratio. Pixel count is more informative than compressed upload size for anticipating image-processing work: a highly compressed file can still decode into a large raster. Set dimension and resource limits before decoding untrusted files, as the image library documentation advises.
How do you prove metadata is gone?
Do not treat a successful write as proof. Add a post-encode audit that reads each public artifact and rejects unexpected metadata or dimensions before changing the database state to publishable. Keep the allowlist tiny. Width, height, format, color space, and page count are operational properties; user comments, GPS data, camera serials, and arbitrary profiles are not required for delivery.
The audit result should record the asset ID, policy version, input hash, canonical hash, crop specification, encoder version, duration, and pass or fail state. It should not copy the metadata values into logs. Logging a rejected GPS value merely moves private data from an image into a system with broader retention and access.
Tests need real fixtures. Include all EXIF orientation values, an image with GPS fields, an IPTC credit, an XMP packet, an ICC profile, an animated or multi-page input, a truncated file, and an image whose compressed bytes are small but decoded dimensions exceed policy. Assert both what disappears and what remains visible. For attribution, assert the page renders the database credit even after the image is downloaded and re-encoded.
Keep one golden crop per aspect ratio for regression review, but avoid pixel-perfect assertions across encoder upgrades. Assert dimensions and policy invariants in every run; use perceptual comparison with an explicit tolerance for composition changes. A crop that is technically valid but removes the receipt total is still wrong.
Operations without leaking the source
Deployment starts with two storage classes: private uploads and public derivatives. Workers may read the private class; the delivery layer may read only public objects. Use random asset identifiers, make crop jobs idempotent, and write to a temporary object before an atomic publish step. A retry must replace the same logical variant rather than create another public URL.
Short-lived source retention limits exposure, but the exact duration is a legal and product decision, not a universal number. Deletion also needs evidence: record the source object's identifier and deletion state without retaining its metadata. If a dispute requires original evidence, isolate that retention path from normal image delivery and document who can access it.
Watch queue age and failed audits separately from encoder errors. They answer different questions. Queue age shows whether new uploads are waiting; audit failures show that files were produced but cannot be published. Alerting on one combined failure count makes privacy regressions look like ordinary capacity noise.
Measure both.
The release checklist is short in prose. Before enabling a new format or crop, confirm the decoder limits, orientation behavior, metadata defaults, output audit, cache key, and deletion policy. Run the hostile fixtures. Verify that the public role cannot read originals and that attribution still appears from application data. Then ship one ratio, observe latency and output size, and expand only when a real layout needs another.
The durable rule is clear: sanitation happens once, before publication; crops may happen later, but only from sanitized pixels. That gives attribution a stable home and makes privacy independent of cache behavior, layout churn, or a forgotten encoder flag.
References
- https://developer.mozilla.org/en-US/docs/Web/Media/Formats/Image_types
- https://sharp.pixelplumbing.com/api-output/
- https://sharp.pixelplumbing.com/api-operation/#rotate
- https://sharp.pixelplumbing.com/api-input/#metadata
- https://www.iptc.org/std/photometadata/specification/IPTC-PhotoMetadata
- https://owasp.org/www-community/vulnerabilities/Unrestricted_File_Upload













