Skip to content
This repository was archived by the owner on Sep 27, 2026. It is now read-only.

Security Model

Syed Ahmer Shah edited this page Sep 27, 2026 · 2 revisions

Security Model

To report a vulnerability, follow SECURITY.md — never a public issue.

Authentication

  • Argon2id hashing (64 MiB, 4 passes); legacy bcrypt hashes upgrade on next sign-in.
  • Password policy: mixed case, number, symbol; production also rejects known-breached passwords.
  • E-mail verification with a six-digit code: hashed, single-use, 15-minute life, 5 attempts, row-locked; failed attempts persist even when the request errors.
  • New-device sign-in alert e-mails; password-change alerts; sign out other devices (database sessions).
  • Session ID regenerated at sign-in; cookies encrypted, HttpOnly, Secure, SameSite=Lax.

Authorisation

  • role:customer|farmer|admin middleware on every private area; verified before ordering or listing; farmer.approved before a farmer can list or take orders.
  • Ownership checks on orders, reviews, favourites and family links.

Abuse and bots

Named rate limiters (per minute unless noted):

Limiter Limit Used on
login 10 sign-in
register 3 sign-up
password 3 reset & password changes
verify 6, 10/hour code entry
resend 1, 6/hour code resend
contact 3 contact form
assistant 20 Basket Buddy
search 120 live search
cart 90 basket sync
prefs 20 language preference
writes 60 every customer/farmer write

Plus account + IP lockout with growing cool-downs, a site-wide limiter on every page (300 requests a minute per IP for guests, 600 per signed-in account), and BotGuard — four layers on every public form (App\Support\BotGuard):

  1. Honeypot — a field people never see; anything typed into it is a bot.
  2. Signed form ticket — every page render carries a fresh ticket: the issue time plus random noise, encrypted and MAC'd with APP_KEY (random IV, so no two tickets look alike). A form sent back in under 2 seconds, after 2 hours (BOT_FORM_TTL_SECONDS), or with a missing, forged or edited ticket is refused. This replaced a timestamp the browser supplied, which a script could simply fake.
  3. Invisible reCAPTCHA v3 scoring (RECAPTCHA_MIN_SCORE, default 0.5).
  4. A visible challenge — Cloudflare Turnstile or the reCAPTCHA v2 checkbox (CAPTCHA_CHALLENGE=turnstile|recaptcha; Turnstile wins when both are configured). Required on sign-up, contact and both password-reset forms; shown on sign-in after a low v3 score; shown on every guarded form when v3 isn't configured. Turnstile tokens are checked for the right action and hostname and sent with an idempotency key.

If Google or Cloudflare can't be reached, the form fails open by default (CAPTCHA_FAIL_OPEN=true) so an outage never locks people out — the honeypot, the ticket and the rate limits still apply. The published demo accounts skip the captcha (their passwords are public) but keep the honeypot and lockout.

Browser hardening

  • Nonce CSP with a new random nonce on every response (HTML is never cached by the service worker, so a refresh always brings a new one), strict-dynamic, object-src 'none', base-uri 'self', frame-ancestors 'self'; upgrade-insecure-requests only over HTTPS.
  • HSTS (preload-ready), X-Content-Type-Options, X-Frame-Options, Referrer-Policy, Permissions-Policy, X-Permitted-Cross-Domain-Policies. COOP is opt-in (SECURITY_COOP=true).

Data integrity

  • CSRF on every state change, and the token is regenerated on every full page load (a refresh or a new tab gets a new CSPRNG token). Inertia and the fetch helper read the XSRF-TOKEN cookie at the moment they send, so other open tabs keep working (FreshCsrfToken).
  • Cross-site writes are refused before any controller runs: a POST / PUT / PATCH / DELETE that the browser marks Sec-Fetch-Site: cross-site gets a 403.
  • The only CSRF / cross-site exemptions are webhooks/resend/inbound (valid Svix HMAC signature and a timestamp within 5 minutes) and payments/callback/* (HMAC-SHA256 signature and matching amount).
  • Server-side price, stock, coupon and capacity calculation with row locks (see Order Lifecycle).
  • Uploads: browser-side WebP compression, then server MIME/dimension/pixel checks, GD decode + re-encode (strips EXIF/GPS and trailing payloads), random names; SVG rejected.
  • Eloquent bindings everywhere — no raw user input in SQL.

Payments

Amounts recomputed on the server, a locked pending → processing state machine, one-time idempotency keys, HMAC-verified callbacks, automatic refunds for late captures, and card numbers / CVCs never stored. Details: Payments.

Social sign-in

Google and Facebook via OAuth 2.0 with the state parameter against login-CSRF; only verified provider e-mails are trusted; pre-account takeover is blocked (an unverified password on the same e-mail is wiped and its sessions revoked); avatars are fetched only over HTTPS from provider hosts with every redirect re-checked (no SSRF) and re-encoded; no access or refresh tokens are stored.

The assistant

Basket Buddy accepts only whitelisted intent keys — no free text reaches the server, so there's nothing to prompt-inject — and every personal answer is built from the signed-in user alone.

Repository protection

main is the only branch. Rulesets block creating any other branch in this repository, block deleting or force-pushing main, require a linear history and squash merges, and require all four CI checks (tests on PHP 8.2 and 8.3, the dependency audit and the Playwright run) to pass on an up-to-date branch. Tags are locked too.

Privacy

Card numbers are never stored, no advertising or analytics cookies, minimal retention (see the Privacy Policy).

Supply chain

CI runs composer audit and npm audit --audit-level=high on every push and pull request.

Clone this wiki locally