Skip to content
Y
YAS.SH
Docs

08_SECURITY

08 — SECURITY

v3.1.0 — post-audit edition. This document describes the security architecture as shipped after the 2026-08-07 enterprise audit. The findings/fixes evidence lives in SECURITY_AUDIT_REPORT.md at the repo root; verification commands are at the bottom of this file.

1. Principles

  • Zero trust / defense in depth: every request is authenticated and authorized server-side; client code is never consulted for permissions.
  • Secure by default: deny schemes/aliases/inputs unless allow-listed.
  • Least privilege: API keys carry enforcement scopes; key-management is session-only so a leaked key cannot self-propagate.
  • No secrets in the repo: .env* ignored; provider keys (Web3Forms) and session salt live only in the environment.
  • Fail closed, fail quiet: RFC 9457 problem bodies, never stack traces, SQL, or paths.

2. Identity & Access

Control Implementation
Passwords bcryptjs 3, cost 12; 72-byte truncation explicitly rejected; breached-password denylist at registration
Login throttles 15/min per IP and 10/5 min per credential; dummy-compare evens timing (anti-enumeration)
Sessions 256-bit opaque tokens, SHA-256 hashed at rest, 30-day absolute expiry, lazy purge of expired rows, destroyed server-side at logout
Session cookie __Host-yas_session in production (HttpOnly, Secure, SameSite=Lax, Path=/, no Domain) / yas_session in dev
API keys yas_live_ + 128-bit entropy, hash-at-rest, shown once, revocable, expirable, scopes enforced (links:read, links:write, analytics:read)
Key management /api/v1/api-keys* require a browser session — keys cannot manage keys
CSRF SameSite=Lax plus Origin≡Host guard on every cookie-authenticated mutation (403 CSRF_BLOCKED)
Roles USER/ADMIN column present; no hidden admin endpoints exist by design
Brute force (v3.2.5) Per-account and per-IP lockout after failed logins: 5 → 60s, 10 → 5 min, 15+ → 15 min (lib/brute-force.ts); successful login resets the counter; returns 429 ACCOUNT_LOCKED
reCAPTCHA (v3.2.5, optional) Google reCAPTCHA v3 on login/register; server-side siteverify with score ≥ 0.5 and action binding; fails closed when enabled (lib/recaptcha.ts)
OAuth/OIDC (v3.2.5, optional) Google, GitHub, Apple Authorization Code flow; random state nonce stored in a SameSite=Lax httpOnly cookie and verified on callback (login-CSRF); accounts linked by verified email (lib/oauth.ts)

3. Input / Output Safety

  • Every write endpoint parses with zod (strict schemas for PATCH), has a JSON size ceiling (readJson, 5–100 KB per route) and returns 413/400/409 — never 500 — on bad input.
  • URLs: http/https only, ≤2048 chars, no control/whitespace chars (destinationUrlSchema) — kills javascript:, data:, CR/LF smuggling.
  • Aliases: ^[a-z0-9-_]{3,30}$, reserved-namespace denylist; shortCode is immutable so alias edits never break published links.
  • Markdown → HTML: single sanctioned path lib/markdown.ts = marked + sanitize-html (strict tag/attr/scheme allow-list, rel=noopener noreferrer + lazy images, script/iframe/object/form dropped with contents). Regex "sanitizers" are banned.
  • Emails: CR/LF/control chars stripped from single-line fields (header injection defense) before relaying to the provider.
  • CSV export: formula-injection escaping (', quoting) + 10k row cap.

4. HTTP & Transport

  • CSP, HSTS (2y preload), X-Frame-Options: DENY, nosniff, Referrer-Policy: strict-origin-when-cross-origin, Permissions-Policy — set centrally in next.config.mjs and at Nginx.
  • CSP: 'unsafe-eval' removed; object-src 'none'; upgrade-insecure-requests in prod. script-src 'self' 'unsafe-inline' is the documented residual (prerendered pages can't carry nonces — see SECURITY_AUDIT_REPORT §12 R-1).
  • /api/* gets Cache-Control: no-store; 302/401/410 redirect responses are also no-store (analytics integrity + no replay).
  • All 429s carry Retry-After.

5. Secrets & Config

  • SESSION_SECRET (32+ random chars required) — salts IP hashes in analytics; rotation does not invalidate sessions.
  • WEB3FORMS_ACCESS_KEY — server-side only (contact form).
  • SEED_ADMIN_PASSWORD — required explicitly; seed refuses defaults.
  • No git history secrets (verified), .gitignore blocks .env*, dev.db, certs; compose has zero hardcoded credentials.

6. Abuse, DoS & Rate Limiting

Surface Limit
register/login 20/min + 15/min per IP; +10/5min per credential
link create 60/min user, 120/min IP
key create 10/min user
contact 5/min IP + honeypot
newsletter 10/min IP + honeypot
tools / qr 60/min IP, 64 KiB cap, allow-listed algos, ReDoS guards
redirect GET / POST 240/min and 60/min IP; 10/min per-link password
analytics scope-gated; DB-side aggregation (no row materialization)

In-memory limiter is per-instance and bounds its own memory (60s sweep, 50k cap). Multi-node → move buckets to Valkey/Redis (REDIS_URL, see §12 R-2 of the audit report).

7. Infrastructure (Oracle Linux 9)

TLS 1.2/1.3 + OCSP stapling, HSTS preload, X-Real-IP overwrite (trusted IP source), client_max_body_size 2m, edge limit_req zones, fail2ban, firewalld (22/80/443), SELinux enforcing, systemd ProtectSystem=strict + NoNewPrivileges + PrivateTmp, journald rotation, daily DB backups (deploy/backup.sh), dnf-automatic security updates. Full snippets: docs/15_OPERATIONS_ORACLE_LINUX.md, deploy/nginx-yas.conf.

Client-IP trust (v3.2.5): lib/api-helpers.ts#getIp() trusts X-Real-IP/X-Forwarded-For/CF-Connecting-IP only when TRUST_PROXY=true (production; Nginx overwrites X-Real-IP with $remote_addr, so callers cannot spoof it). Outside a trusted proxy (local dev / a directly-exposed standalone server) the app reports the genuine connection address and ignores forged headers — preventing IP spoofing for rate limiting and analytics.

8. Verification Battery (run before every release)

pnpm typecheck && pnpm lint && pnpm build       # gates 1–3
pnpm audit --prod                                # gate 4: 0 vulns
bash scripts/grep-gates.sh                       # gate 5: banned patterns
BASE=$URL pnpm smoke                             # gate 6: 57 functional checks
BASE=$URL pnpm smoke:security                    # gate 7: 18 adversarial checks
🍪 Cookies & privacy. yas.sh uses only essential cookies to keep you signed in and remember your preferences. We do not run third-party trackers. See our cookie policy and privacy policy.
Settings