Browser Storage Upload: Webhook Notification Rules for Private-Bucket Retention, Thumbnails, and Virus-Scan Processing
Short answer: let the browser upload a signed object into a private quarantine bucket, then let a verified storage event create the scan and thumbnail job; the application, not the browser, should enforce the deletion de
Short answer: let the browser upload a signed object into a private quarantine bucket, then let a verified storage event create the scan and thumbnail job; the application, not the browser, should enforce the deletion deadline.
That decision fits a customer-support system retaining signed documents. It also draws a line that is easy to miss: an upload being durable does not mean the file is safe to show, and a successful scan does not mean the file may be retained forever. I would record the deadline beside the document before issuing the upload authorization, then make every later state transition carry that deadline.
What must the retention-first upload architecture guarantee?
The first invariant is private ingress. The browser gets authorization for one object key and one bounded upload, while the storage credential stays on the server. The key should be generated by the application rather than derived from the customer filename; OWASP recommends generated filenames, allowlisted types, size limits, and storage outside the web root for file-upload systems.
The second invariant is quarantine. A completed browser request may move a record to uploaded, but it must not make the document downloadable. A worker can verify the object, inspect its declared and detected type, run a virus scan, and create a thumbnail only after the event has been accepted as authentic. The original stays private even after approval; the application can issue a short-lived read authorization when a support agent is allowed to view it.
The third invariant is immutability between stages. Write derived thumbnails to new keys, keep the original key stable, and never use a customer filename as an output destination. That makes a retry legible: it can calculate the same destination from the document ID and revision instead of guessing which prior write was correct.
The fourth invariant is a deletion deadline with an owner. The database should store delete_at, the current processing state, and the object keys. A scheduled deletion worker can remove the original and derivatives, mark the record deleted, and emit an audit entry. If deletion is legally sensitive, the audit record should contain the document ID and action time rather than a copy of the signed file. Make the deadline part of the job's input, too: a retry after a long queue delay must compare the clock before it creates another derivative, and a deletion retry must be able to remove a key even when the database already says deleted. I would also keep an explicit document revision, because a support agent replacing a signature should not cause an old event to process against the new record. That small bit of bookkeeping is less exciting than the upload path, but it is where retention bugs become discoverable instead of mysterious.
The deadline matters.
These are separate controls. A private bucket does not create a retention policy. A webhook does not prove that a payload is authentic. A virus scan does not solve authorization.
How can browser storage uploads trigger private-bucket webhooks for thumbnails and virus scans?
Use the event as a durable handoff, not as a command from the browser:
- The application creates a document row with its owner, generated object key, and deletion deadline.
- The browser uploads directly to the private object location.
- Storage sends an event after the object is present.
- A small webhook adapter authenticates and normalizes that event, inserts a deduplicated job, and returns quickly.
- A worker reads the job, checks that the key belongs to the document, scans the original, and writes thumbnails under derived keys.
- The worker marks the document approved only when the required checks succeed; a separate deadline worker removes all related keys when
delete_atarrives.
Do not ask the browser to call the scanner after upload. A tab can close between those two requests, and a client retry cannot know whether storage committed the bytes. The bucket event observes the state the worker actually needs.
Here is the critical boundary in Python. The payload schema and authenticity check belong in the provider-specific adapter; the job table below is deliberately generic. Hashing the exact event body is a useful fallback identifier for a simple test harness, but a production adapter should prefer the event ID supplied by its notification contract when one exists.
import hashlib
import json
import sqlite3
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
DATABASE = "support_uploads.db"
def initialize() -> None:
with sqlite3.connect(DATABASE) as connection:
connection.execute(
"""
CREATE TABLE IF NOT EXISTS upload_jobs (
event_id TEXT PRIMARY KEY,
object_key TEXT NOT NULL,
payload TEXT NOT NULL,
state TEXT NOT NULL CHECK (state IN ('queued', 'done'))
)
"""
)
def enqueue(body: bytes) -> bool:
event_id = hashlib.sha256(body).hexdigest()
event = json.loads(body)
object_key = event["object_key"]
payload = json.dumps(event, separators=(",", ":"))
with sqlite3.connect(DATABASE) as connection:
result = connection.execute(
"INSERT OR IGNORE INTO upload_jobs"
"(event_id, object_key, payload, state) VALUES (?, ?, ?, 'queued')",
(event_id, object_key, payload),
)
return result.rowcount == 1
class StorageEventHandler(BaseHTTPRequestHandler):
def do_POST(self) -> None:
if self.path != "/storage-events":
self.send_error(404)
return
try:
length = int(self.headers.get("Content-Length", "0"))
body = self.rfile.read(length)
created = enqueue(body)
except (ValueError, KeyError, json.JSONDecodeError):
self.send_error(400, "Expected a storage event")
return
response = json.dumps({"accepted": True, "new_job": created}).encode()
self.send_response(202)
self.send_header("Content-Type", "application/json")
self.send_header("Content-Length", str(len(response)))
self.end_headers()
self.wfile.write(response)
if __name__ == "__main__":
initialize()
ThreadingHTTPServer(("127.0.0.1", 8080), StorageEventHandler).serve_forever()
The example is intentionally incomplete at the trust boundary: production code must authenticate the notification before inserting the job, validate that the object key has the expected tenant and document prefix, and reject an event whose size or metadata violates the upload policy. Those checks are not decoration. A notification endpoint that accepts arbitrary object keys can become an internal job injection endpoint.
Which failure boundaries decide the design?
The useful table is about recovery, not feature count.
| Boundary | Failure mode | Containment rule |
|---|---|---|
| Browser to object storage | The tab disappears or retries | Treat the object event as authoritative; reconcile an uploaded row separately |
| Storage notification to job table | Delivery is delayed or repeated | Authenticate, insert with a unique event key, and acknowledge only after durable insertion |
| Job to scanner and thumbnailer | A worker stops halfway through | Use retryable states and deterministic output keys; never promote the original by accident |
| Approved object to agent download | An untrusted or unauthorized file is exposed | Gate reads through application authorization and short-lived access |
| Deadline worker to deletion | One derivative is forgotten | Store all related keys and make deletion idempotent; retain an audit event |
Duplicate events are normal enough to design for. A unique event constraint prevents two queue rows, but it does not prevent two workers from racing after a lease expires. The worker therefore needs an ownership lease or an atomic state transition, and its writes need stable names. If a thumbnail already exists, verify that it belongs to the same document revision before treating it as complete.
Bad input is routine. A document can claim to be a PDF while containing something else, and a filename extension is not a security boundary. Follow the upload controls in the OWASP guidance, keep limits explicit, and quarantine files before any parser or image library handles them. A rejected scan should become a business state with a reason safe for support staff, while the original remains inaccessible and subject to the same deletion deadline.
Observability needs the document ID, event ID, job attempt, state transition, and deadline. It does not need the signed content. Alert on jobs that approach their deadline while still queued, because retention enforcement should not silently depend on a healthy thumbnail worker.
What should a team compare before choosing storage and event plumbing?
The comparison axis is the retention boundary. Ask whether the selected system can keep objects private, deliver a verifiable notification, expose the operations needed by the deletion worker, and support a restore test that matches the support team's recovery objective. Familiar upload syntax is not evidence for any of those answers.
| Capability to verify | Why it matters for signed support documents |
|---|---|
| Direct browser authorization | Limits server bandwidth without handing a reusable credential to the browser |
| Private reads and writes | Prevents an uploaded document from becoming an accidental public URL |
| Notification authenticity and retry behavior | Determines whether the job table can trust and replay events |
| Lifecycle and deletion operations | Makes delete_at enforceable for originals and thumbnails |
| Recovery and export procedure | Gives the team a tested answer for accidental deletion or provider loss |
The catch is that an event-driven path is not suitable when the product requires a synchronous, human-visible verdict before the upload request completes, or when the selected storage service cannot provide a notification contract the team can authenticate. In those cases, use a narrowly bounded application proxy or choose storage with the required event and retention controls. Keep the simpler path when files are tiny and the extra asynchronous machinery would exceed the system's actual risk.
I'm not sure a generic signature recipe can be safe across storage systems; the selected notification specification must settle that detail. Your mileage may vary on restore design too, because a customer-support archive with a contractual deletion deadline has different recovery obligations from a disposable media cache.
The rejected option is βscan in the browser response.β It couples browser connectivity, object persistence, scanning time, thumbnail work, and database state into one request. That can be reasonable for a small internal tool with a strict size cap. It is a poor default for signed customer records whose deletion clock must keep running when no browser is open.
References
Originally published by Dev.to Security. Aggregated on AIWithGhost for educational purposes β full credit and traffic to the original publisher.