When you host your own workspace, security isn’t someone else’s problem.
In a standard SaaS workspace (like Slack or Microsoft Teams), a massive team of engineers manages global file quarantine, antivirus scanning, and secure request isolation. But when a team pulls their workspace onto their own secure local hardware, the security surface area shifts entirely to the self-hosted binary.
If a user uploads a malicious script, a corrupted zip archive, or a Trojan horse into a chat channel, your private server or local VPS is the immediate execution target.
Over the last 6 days, I undertook a deep, zero-trust security audit of OneCamp.
In this post, I will walk you through the engineering behind our new zero-trust ingestion system—including peeking file magic-bytes for MIME validation, scanning uploads with live ClamAV socket pipes, preventing zip-slip and zip-bomb exploits during Slack imports, and building an exponentially-backed-off, visibility-aware polling engine on the frontend to keep clients resilient during server deployments.
In many web applications, checking a file upload is dangerously simple: the backend reads the client-supplied Content-Type header or inspects the file extension (.png, .pdf) and trusts it.
This is a massive security vulnerability. A malicious attacker can easily rename a dangerous executable or an XSS-carrying HTML file (e.g., payload.exe or xss.html) to photo.png, upload it, and exploit the server or downstream users’ browsers when it’s rendered inline.
To resolve this, I built the Safe Upload Pipeline (uploadsafe.go).
+------------------------------------+
| Incoming File Stream |
+------------------------------------+
|
+---------------v---------------+
| Peek first 512 Bytes | (Using bufio.NewReaderSize)
| Detect Magic Byte Signature |
+---------------v---------------+
|
+----------------v----------------+
| Verify Extension vs. Signature |
+----------------v----------------+
|
+---------------v---------------+
| Coerce Dangerous Types to | (e.g., HTML, SVG -> octet-stream)
| application/octet-stream |
+---------------v---------------+
When a file is uploaded, the backend must read its leading bytes to identify the true file signature. However, reading the entire file into memory to detect the MIME type causes severe memory spikes (especially for large video shares).
We solve this by peeking the stream:
bufio.NewReaderSize(body, 512).http.DetectContentType contract)..png contains standard HTML headers or binary ELF code), the upload is immediately rejected with ErrSignatureMismatch.Certain file extensions (like .html, .xml, .js, and .svg) can carry malicious Javascript payloads. If an SVG file is rendered inline by a browser, the embedded script executes within the context of your application, leading to a direct Stored Cross-Site Scripting (XSS) vulnerability.
To neutralize this risk:
inlineSafeTypes: png, jpeg, gif, webp, bmp, and ico). Note that SVG is explicitly excluded.dangerousExtensions list (such as .html, .svg, .wasm, .exe, or .sh), the Content-Type is immediately coerced to application/octet-stream.Content-Disposition: attachment; filename="untrusted.svg"; filename*=UTF-8''untrusted.svg
Because it is served as an attachment with a raw stream MIME type, modern browsers will always save it to disk instead of executing or rendering it, completely eliminating stored-XSS threats.
For self-hosted deployments, data migration is a primary ingestion channel. In my previous post about our universal import engine, I broke down how we stream data from external platforms. But accepting massive, multi-gigabyte Zip archives from administrators requires absolute infrastructure protection.
avscan.go)To prevent infected attachments from ever touching our long-term storage buckets, I integrated an ambient ClamAV Antivirus Scanner:
zipsafe.go)Decompressing untrusted zip archives presents two classic, high-risk security threats: Zip Slip (directory traversal) and Zip Bombs (infinite decompression loops).
+------------------------+
| Zip Archive Entry |
+------------------------+
|
+-------------------------+-------------------------+
| |
+----------v----------+ +----------v----------+
| Zip Slip? | | Zip Bomb? |
| - Check ".." paths | | - Check compression |
| - Verify in root | | ratio (> 100x) |
| - Abort instantly | | - Check total bytes |
+---------------------+ +---------------------+
../../etc/passwd or relative ../ markers), a naive decompressor will write the file outside the targeted staging directory, potentially overwriting critical system files. We enforce strict root path isolation on every extracted path using filepath.Clean().float64(decompressedBytes) / float64(compressedBytes).100.0 (indicating an anomaly) or the raw decompressed size exceeds a safe threshold (like 2GB for a standard Slack channel history pack), the extractor terminates instantly and rolls back the transaction.Security is also about availability.
If a self-hosted server experiences a routine deploy, an update restart, or a temporary database lockup, thousands of open browser tabs will instantly fail their API requests. If those tabs are configured with basic, naive setInterval() polling loops, they will all bombard the server with reconnection attempts.
When the server finally restarts, it is instantly met with a massive, client-induced Thundering Herd DDoS attack, crashing it again.
To protect the server and keep the client experience seamless, I built the visibility-aware Resilient Polling Hook (useResilientPolling.ts).
export function useResilientPolling(opts: PollingOptions) {
// ... ref and timeout bookkeeping ...
const tick = useCallback(async () => {
if (!enabled || mqttHealthy) return
// Pause if tab is in the background
if (document.visibilityState === "hidden") {
schedule(interval)
return
}
// Cap execution wall-clock time
if (capMs > 0 && startedAt && Date.now() - startedAt > capMs) {
clearTimer()
return
}
try {
await onPoll()
errorCount.current = 0 // Reset on success
schedule(interval)
} catch {
// Exponential Backoff on failure
errorCount.current = Math.min(errorCount.current + 1, 30)
const factor = Math.min(2 ** errorCount.current, maxBackoff)
schedule(interval * factor)
}
}, [enabled, mqttHealthy, interval, capMs])
}
mqttHealthy: true), the hook completely disables its timers. The UI stays fresh via live pushes, saving significant database read cycles on the backend.focus or visibilitychange event), the hook kicks off an immediate, responsive revalidation.By implementing magic-byte signatures, eager antivirus checking, safe decompression boundaries, and visibility-aware client backoffs, OneCamp ensures that self-hosting does not mean compromising on security.
You get the isolation of running on your own physical hardware, with the enterprise-grade ingestion protection you would expect from a massive cloud platform.
The zero-trust upload security layer and import workers are fully live in the OneCamp backend repository. Dive into the helpers/uploadsafe and hooks/useResilientPolling.ts files to explore the full Go and TypeScript implementations!
Previous posts: Active Memory Layering: How OneCamp Orchestrates GraphRAG and Vector Databases · Universal Import Engine: Migrating from 8 Platforms · Why MQTT for Real-time Sync · Building the Anti-SaaS Workspace
For security audits and engineering deep-dives, follow me on Twitter.