How FolioDoc Keeps Your Documents Secure: A Technical Deep-Dive
We don't just say "we take security seriously." Here's exactly what we do — and what we don't do yet.
Security Isn't a Feature — It's the Foundation
If you've evaluated SaaS products before, you've probably seen the same line a hundred times: "We take security seriously." It's on every marketing page, usually followed by a padlock icon and nothing else. We wanted to do something different with FolioDoc.
This article walks through the actual security measures we've implemented — specific algorithms, concrete limits, real architectural decisions. If you're a CTO, IT manager, or compliance officer trying to figure out whether FolioDoc is safe enough for your organization's documents, this is for you. We'll also tell you what we haven't built yet, because that honesty matters more than any marketing claim.
Everything Travels Over TLS
Every connection to FolioDoc is encrypted with TLS 1.2 or higher. We use modern cipher suites — ECDHE for key exchange, AES-GCM and CHACHA20-POLY1305 for symmetric encryption. Legacy protocols like TLS 1.0 and 1.1 are disabled entirely. There are no exceptions and no fallback to plaintext HTTP.
We enforce HSTS (HTTP Strict Transport Security) with a max-age of 31,536,000 seconds — that's one full year — and we've opted into the HSTS preload list. Once your browser has visited FolioDoc, it won't even attempt an unencrypted connection. We also enable OCSP stapling so your browser can verify our certificate's validity without making a separate round-trip to the certificate authority.
Why does this matter for a document collection tool? Because magic links, file uploads, and authentication tokens all travel over the wire. If any of that happens over HTTP, an attacker on the same network could intercept tokens, hijack sessions, or tamper with uploaded files. TLS everywhere is the baseline, not a bonus.
Magic Links: No Passwords, No Accounts
When you send a document request through FolioDoc, your recipients don't need to create an account or remember a password. Instead, they receive a magic link — a unique URL that gives them direct access to their upload portal. This removes a huge source of friction and eliminates the security risks that come with password-based authentication for external users.
Here's how it works under the hood. We generate each token using Python's secrets.token_urlsafe(32), which produces a cryptographically random 32-byte string encoded in URL-safe Base64. That gives us 256 bits of entropy — brute-forcing it isn't practical. Before we store anything in the database, we run the token through SHA-256. The raw token is only ever present in the URL we send to the recipient. Our database never contains a value that could be used to reconstruct the link.
Each magic link expires 24 hours after the request's deadline. We also track how many times a link has been accessed. If someone forwards the link or it leaks, you'll see the access count climb — a simple but effective signal that something might be off.
This approach is intentional. Asking a client or external partner to "create an account and set a password" to upload a few documents is bad UX and bad security. People reuse passwords, forget them, and get frustrated. A one-time link scoped to a specific request is simpler and safer.
Five Layers of File Validation
Accepting file uploads from the internet is inherently risky. A file called "invoice.pdf" might actually be an executable. A 2GB file could be a denial-of-service attempt. We validate every upload through five sequential checks before it's accepted.
Here's the full pipeline:
- Size check: Every file must be under 25MB. This is enforced server-side, not just in the browser.
- Content-Type allowlist: We only accept specific MIME types — PDF, JPEG, PNG, GIF, DOC, DOCX, XLS, XLSX, and CSV. Everything else is rejected.
- Extension validation: The file extension must match one of our allowed types. A file with no extension or a disallowed extension is blocked.
- Magic-byte inspection: This is the important one. We use the filetype library to read the first bytes of the file and determine its actual format from the binary header. If someone renames malware.exe to invoice.pdf, the extension says PDF but the magic bytes say PE executable — and we reject it.
- Per-recipient quotas: Each recipient can upload at most 10 files totaling no more than 100MB. This prevents any single recipient from flooding a request with data.
These checks run in order. If a file fails at any layer, it's rejected immediately — we don't waste resources running later checks on a file that already failed an earlier one.
We also compute a SHA-256 checksum on every uploaded file and run daily integrity checks. If a file has been corrupted or tampered with, we catch it.
Authentication and Rate Limiting
For account holders (the people creating and managing document requests), we use JWT-based authentication. Access tokens expire after 15 minutes. Refresh tokens last 7 days and are rotated on each use — once a refresh token is used, the old one is blacklisted and can never be used again. This limits the damage window if a token is compromised.
We rate-limit aggressively on sensitive endpoints. Login attempts are capped at 5 per 15 minutes, scoped to the combination of email address and IP. This means an attacker can't just try thousands of passwords against your account. The general API rate limit is 200 requests per minute per authenticated user — high enough for normal use, low enough to stop automated abuse.
Password reset emails have a 5-minute per-email cooldown. If you trigger a reset, you can't trigger another one for the same email for 5 minutes, regardless of which IP the request comes from. This prevents mailbox flooding. Recipient notification emails have a similar 10-minute cooldown to prevent spamming external recipients.
What Happens When You Delete Something
When you delete a request in FolioDoc, we don't just flip a flag in the database. We perform a full cascade deletion: all checklist items, recipient records, uploaded responses, magic links, notification history, and escalation events tied to that request are removed. Physical files on disk are explicitly deleted before the database cascade runs.
If a file deletion fails — maybe the storage backend is temporarily unreachable — we don't silently skip it. Instead, we create a PendingFileDeletion record that gets retried automatically. A scheduled task picks up these records and retries them. Files are never quietly orphaned on disk.
Every deletion is recorded in a DeletionLog with a timestamp, who triggered it, what was deleted, and how many records were affected. We don't log sensitive content — just enough to maintain an audit trail.
For GDPR compliance, completed and expired requests are automatically purged after 90 days (configurable via environment variable). Account deletion removes everything — all requests, all files, the brand logo, all of it. We also provide self-service data export in JSON and CSV formats so you can exercise your right to data portability before deleting anything.
Security Headers and Output Sanitization
We set Content-Security-Policy headers that block object embeds and frame embedding (object-src 'none', frame-ancestors 'none'). X-Content-Type-Options is set to nosniff to prevent browsers from guessing MIME types. Referrer-Policy is set to no-referrer so we never leak URLs — including magic link tokens — in referrer headers.
When you download files, we force the Content-Type to application/octet-stream. This tells the browser to download the file rather than trying to render it inline, which prevents a class of attacks where a malicious HTML file disguised as a document could execute JavaScript in the context of our domain.
CSV exports are sanitized against formula injection. Any cell value starting with =, +, -, or @ is prefixed with a tab character to prevent spreadsheet applications from interpreting the value as a formula. This stops an attacker from injecting something like =HYPERLINK("http://evil.com","Click here") into a field that later gets exported.
Antivirus Scanning with ClamAV
On top of our five-layer file validation, we now scan every uploaded file with ClamAV, the widely trusted open-source antivirus engine. Scans run asynchronously right after upload — your recipients won't notice any delay, but behind the scenes we're checking the file against ClamAV's signature database. If a file is flagged as infected, we delete it immediately and mark the upload as rejected. No infected file ever makes it into your request.
That said, we want to be honest: no antivirus solution catches 100% of threats. New malware variants appear constantly, and there's always a window between a threat emerging and signatures being updated. ClamAV is a strong layer of defense, but it's not a guarantee. We still recommend that you and your team verify files on your own machines with up-to-date endpoint protection before opening them. Think of our scanning as one more layer in the stack, not a replacement for your own security practices.
What We Don't Do (Yet)
We believe being upfront about gaps is more useful than pretending they don't exist.
We don't offer end-to-end encryption. Files are encrypted in transit via TLS and can be encrypted at rest depending on your hosting configuration (e.g., EBS encryption on AWS), but we don't implement client-side encryption where only the recipient holds the decryption key. For most document collection workflows, TLS plus at-rest encryption is sufficient, but we want to be clear about the boundary.
Interested in the full details? Check our /security page for the complete list, or review our Data Processing Agreement at /dpa.
Security is never done. We continuously review our implementation, track new vulnerabilities, and update our dependencies. If you have questions about any of this, or if you've found something we should fix, reach out at security@foliodoc.com. We'd rather hear about it from you than from an incident report.