Security Policy
VeriWorkly's security controls, data boundaries, and responsible disclosure process.
Security Policy
VeriWorkly is open source, so its security posture is auditable rather than asserted. This page describes the controls actually implemented in the codebase.
Responsible disclosure
If you believe you have found a security vulnerability, please report it privately.
How to report
- Email: [email protected]
- Do not open a public GitHub issue for a security-sensitive finding. Non-sensitive issues can go to the issue tracker as normal.
What to expect
| Stage | Timeline |
|---|---|
| Acknowledgement | Within 24 hours, with a point of contact assigned to your report. |
| Triage | Severity assessed and a fix plan communicated. |
| Patch | Critical vulnerabilities targeted within 14 days. |
| Disclosure | Coordinated with you once a fix has shipped. |
Please give us a reasonable window to ship a fix before disclosing publicly. The full disclosure policy is published at veriworkly.com/security.
Data boundaries
The single most useful thing to understand about VeriWorkly's security model is what never leaves your device.
| Stays local | Goes to the server |
|---|---|
Document editing and storage (localStorage) | Cloud sync, when enabled |
| PDF, DOCX, HTML, Markdown, text, and JSON generation | Uploaded file text extraction (pdf-parse, mammoth) |
| Live preview rendering | AI generation and ATS analysis |
| Guest sessions | Share-link creation and portfolio publishing |
Because all six export formats are produced in the browser, downloading a resume involves no server round-trip for your document content at all.
Authentication
- Passwordless. Email OTP plus Google, GitHub, and LinkedIn OAuth. There is no account password to reuse or leak.
- OTP hardening — codes expire after 5 minutes, allow 3 failed attempts, and are rate-limited to 3 sends and 5 verifications per minute.
- Session cookies are
HttpOnly,Securein production, andSameSite=Lax. - Cache invalidation — sign-out, session revocation, email change, and account deletion all immediately clear cached session data, so a revoked session cannot survive its cache window.
Secrets and credentials
Different secret types use deliberately different handling:
| Secret | Treatment | Rationale |
|---|---|---|
| Share-link passwords | scrypt, verified with a timing-safe comparison | User-chosen, low-entropy — needs a deliberately slow hash. |
| API keys | HMAC-SHA256 with a dedicated secret | Machine-generated, high-entropy — looked up by hash, so it must be deterministic and fast. |
API_KEY_HASH_SECRET must be distinct from AUTH_SECRET in production; the boot validator refuses
to start otherwise. API keys are stored only as hashes and are invalidated within minutes when the
owning subscription lapses.
Rate limiting
Tiered and Redis-backed, with a bounded in-memory fallback if Redis is unreachable:
| Surface | Limit |
|---|---|
| General API | 100 requests / 15 minutes |
| Authentication routes | 20 requests / minute |
| OTP send | 3 / minute |
| OTP verify | 5 / minute |
| Share password verification | 3 / 5 minutes — makes brute-forcing a share password impractical |
| Public contact form | 5 / hour |
| Usage-metric ingestion | 15 / minute |
| AI endpoints | Their own window, plus credit and quota enforcement |
| Per API key | 20 requests / 15 minutes by default |
Dynamic path segments (IDs, UUIDs) are normalised to a placeholder before keying, so requests to different resources under one route share a counter instead of minting unbounded rate-limit keys.
SSRF protection
The ATS checker can fetch a job description from a URL you supply. That outbound fetch is heavily constrained:
- HTTPS only, standard ports only.
- Rejects
localhost,.local, and hostnames with embedded credentials. - DNS-resolves the hostname and rejects all private, loopback, and link-local ranges, including the IPv6 equivalents.
- Pins the resolved IP for the actual request, closing the DNS-rebinding gap between validation and fetch.
- Caps redirects at 3, re-validating every hop.
- Caps the response at 2 MB with an 8-second timeout.
- Requires a minimum amount of extracted visible text before accepting the page.
Upload security
Portfolio images use presigned Cloudflare R2 PUT URLs valid for 10 minutes, followed by a
verify-on-complete step that HEAD-checks the uploaded object's size, content type, and ETag
against what was declared at presign time before marking it usable. This prevents presigning for one
file and uploading something else. Only JPG, PNG, and WebP are accepted, with a 5 MB cap.
Abandoned uploads still PENDING after 24 hours are garbage-collected along with their R2 objects.
Transport and headers
- Helmet applies security headers globally on the API.
- CORS is restricted to an explicit allowlist plus a regex-matched wildcard for
*.veriworkly.comportfolio subdomains. Credentials are only granted to explicitly listed origins, never to wildcard-matched ones. - The portfolio application sets a Content-Security-Policy on published and preview pages,
restricting script and resource origins to same-origin plus
*.veriworkly.com.
Abuse resistance
- View counting deduplicates repeat views from the same IP within a 30-minute window, for both portfolio views and share-link views.
- Webhook idempotency — every billing webhook is recorded by the provider's event ID, so duplicate delivery cannot double-grant credits or entitlements.
- Atomic quota consumption — import and credit mutations use constrained
updateManyoperations rather than read-then-write, eliminating race windows.
Infrastructure
- Database encryption at rest is provided by the managed PostgreSQL host.
- Secrets are supplied as environment variables. In production, use a secret manager (AWS Secrets Manager, Doppler, or your platform's equivalent) rather than flat files.
- Fail-fast configuration — the API validates its production configuration before any worker
starts, so a deployment with a default auth secret, a non-HTTPS auth URL, an over-permissive
TRUST_PROXY, or missing payment/storage credentials fails at boot rather than degrading silently.
Audits
Internal security reviews are ongoing, and third-party audits and community findings are welcome. All application code is public at github.com/VeriWorkly/veriworkly.