Runbooks & opérationsSecurity Roadmap — Q3 2026

Security Roadmap — Q3 2026

Items identifies par l'audit complet du 2026-05-12. Les Critical / High ont ete shippes (frame-src 'self', session-auth migration sur 8 endpoints, cron timing-safe, withInternalAuth prod guard, vercel.json CSP align…

Items identifies par l'audit complet du 2026-05-12. Les Critical / High ont ete shippes (frame-src 'self', session-auth migration sur 8 endpoints, cron timing-safe, withInternalAuth prod guard, vercel.json CSP align, proxy preview headers extension, org-config Data Cache).

Ce doc trace les items effort L restants : pas urgents mais qui elevent la securite / observabilite vers le baseline Vercel 2026+.

1. Vercel Firewall + BotID : Effort S (config dashboard)

Pourquoi

Pas de WAF edge configure aujourd'hui. Le rate-limit applicatif (Upstash) tourne apres l'edge, donc un attaquant peut saturer la latence Lambda avant qu'on reject. BotID + managed rules attrappent les bots avant la function.

Et il ne compte plus par IP partout, ce qui deplace la frontiere plutot que de la fermer. Les routes authentifiees par session comptent contre l'utilisateur depuis security-identity/0364 : l'IP y etait lue en position 0 de x-forwarded-for, donc choisie par l'appelant, donc un seau neuf a chaque requete. Un attaquant non authentifie reste compte sur l'IP de confiance (dernier saut), et c'est precisement le trafic que cette section propose de bloquer avant la function : le limiteur applicatif ne peut pas etre la premiere ligne contre lui, il n'a d'identite fiable qu'apres la session.

Plan

Etape 1 — activer Vercel Firewall (Dashboard → Settings → Firewall)

  • Active OWASP Top 10 Managed Rules (free tier inclus)
  • Active Block Bad Bots
  • Optionnel : ajout custom rules pour /api/auth/* (max 30/min/IP)

Etape 2 — integrer @vercel/botid

pnpm add @vercel/botid
// src/proxy.ts — apres `recordVisit`
import { botid } from "@vercel/botid"

const decision = await botid(req)
if (decision.isBot) {
  return new NextResponse(null, { status: 403 })
}

Cibles prioritaires : /auth, /api/marketplace/listings/* POST, /api/credits/redeem. Pas sur les endpoints publics SEO (blog, sitemap) : googlebot doit passer.

Refs

2. La bibliotheque d'authentification apres next-auth v4 — decision, pas migration

Ce que cette section disait, et pourquoi c'etait faux

Elle planifiait une migration en cinq phases vers Auth.js v5, sur la phrase « Auth.js v5 est GA depuis fin 2024, recommande par Vercel pour Next 16 ». La contradiction etait dans la section elle-meme : sa phase 1 disait pnpm add next-auth@beta. On n'installe pas en @beta ce qui est GA.

Les faits, verifies contre le registre npm le 2026-09-06 :

  • next-auth : dist-tags.latest = 4.24.15 (publie le 2026-07-20), dist-tags.beta = 5.0.0-beta.32. v5 n'est pas sortie de beta.
  • L'annonce officielle nextauthjs/next-auth discussion #13252 du 26 septembre 2025 : « Auth.js (formerly NextAuth.js) will now be maintained by the Better Auth team », « We don't have any immediate plans for v5 », et, pour un nouveau projet, « we recommend Better Auth ».
  • Ce depot est fige sur next-auth 4.24.15 (package.json), un numero desormais derive par pnpm docs:claims : c'est ce qui empeche cette page de re-affirmer une version que le lockfile contredit.

Le risque reel, qui n'etait ecrit nulle part

Ce n'est pas « on a une majeure de retard ». C'est qu'une bibliotheque en mode maintenance (« security and critical fixes ») porte notre unique porte de connexion, et que nous la rustinons deja a la main parce que l'amont ne livre pas dans notre branche : src/modules/auth/normalize.ts et src/proxy.ts portent chacun un durcissement maison (cf. 0128 / 0130). Chaque avis de securite futur suit ce chemin-la.

Ironie a garder en tete avant de proposer Better Auth comme evidence : src/modules/auth/index.ts documente que cette codebase a migre DE Better Auth VERS NextAuth v4.

Ce qu'il faut faire, dans l'ordre

  1. Ne rien migrer sur cette page. Un plan de migration vers une version en beta est un plan qui ne peut pas etre execute, et il occupe la place de la decision qui, elle, doit etre prise.
  2. Ouvrir une ADR « bibliotheque d'authentification 2027 » dans docs/decisions/, avec les trois options reelles et leur cout : rester sur v4 en absorbant les patchs a la main (le statu quo, dont nous connaissons deja le prix) ; Better Auth (l'equipe qui maintient maintenant l'amont, mais un aller-retour) ; une session maison sur jose + Prisma (le moins de surface, le plus de code a nous).
  3. Surveiller le tag latest. scripts/dependency-watch.mjs le fait deja toutes les semaines et reporte une majeure de retard sans la traiter ; le jour ou dist-tags.latest de next-auth passe a 5.x, c'est le declencheur de l'ADR, pas d'un pnpm add.

Refs

3. Playwright e2e — Tenant lateral movement — Effort L

Pourquoi

Notre threat model interdit qu'un user de l'orgA accede aux ressources de l'orgB. Les wrappers getOrgAccess, getStoreAccess, requireOrg sont en place mais aucun test e2e ne prouve qu'ils sont appliques sur chaque endpoint tenant-scoped. Un nouveau endpoint qui oublie le check passe le code review et reste broken en silence.

Plan

Setup fixture multi-user

// e2e/fixtures/tenants.ts
export const tenantA = {
  email: "tenanta@e2e.boostecom.dev",
  orgSlug: "tenant-a",
  storeSlug: "store-a",
  apiKey: process.env.E2E_TENANT_A_API_KEY,
}
export const tenantB = { /* idem */ }

Test suite — chaque endpoint tenant-scoped doit retourner 403/404 quand userA appelle avec orgBId :

test.describe("tenant lateral movement", () => {
  const protectedEndpoints = [
    { path: "/api/organizations/{orgId}", method: "PUT" },
    { path: "/api/organizations/{orgId}/rules", method: "PUT" },
    { path: "/api/organizations/{orgId}/memory/metrics", method: "GET" },
    { path: "/api/stores/{storeId}/integrations/{provider}", method: "GET" },
    { path: "/api/stores/{storeId}/export", method: "GET" },
    // ... ~30 endpoints au total
  ]

  for (const ep of protectedEndpoints) {
    test(`${ep.method} ${ep.path} blocks cross-tenant`, async ({ request }) => {
      const url = ep.path
        .replace("{orgId}", tenantB.orgId)
        .replace("{storeId}", tenantB.storeId)
      const res = await request[ep.method.toLowerCase()](url, {
        headers: { cookie: tenantA.sessionCookie },
      })
      expect([403, 404]).toContain(res.status())
    })
  }
})

CI — run cette suite sur chaque PR qui touche src/app/api/**.

Infra

  • Playwright deja install (@playwright/test dep ? Vérifier)
  • Fixture DB : seed tenantA / tenantB orgs + stores au setup global
  • Cleanup : truncate apres each suite

Refs

4. revalidateTag — Marketplace listings : Effort M (deferre)

Pourquoi (et pourquoi deferre)

listListings() prend un viewerId qui change la query (savedBy overlay). Cacher direct = cacher des resultats user-specific = leak.

Plan complexe :

  1. Cacher la query SANS viewerId avec un tag marketplace:list
  2. Faire le savedBy overlay en memoire apres lecture cache
  3. Invalider depuis seller-actions.ts apres acceptOffer/rejectOffer/etc.

Pour les pages /marketplace/[type] qui sont publiques (anonymous viewer), c'est trivial. Pour /marketplace/[type]/[slug] detail, idem (savedBy n'est pas dans la query principale).

Decision : reporte tant qu'on n'a pas de signal perf (Vercel Speed Insights p95 > 1s sur ces pages). Aujourd'hui le bottleneck est ailleurs.

5. Items secondaires non bloquants

  • process.env.NODE_ENV direct dans src/proxy.ts (3 occurrences) → pas critique mais incoherent avec serverEnv. Les numeros de ligne cites ici jusqu'en 2026-09 (152,175,196) avaient perime : chercher par nom, pas par ligne.
  • Audit inline scripts /workspace:1 CSP violation : probablement un script injecte par extension Chrome user (BoostEcom Spy / similaire), pas notre code. Verifier via /api/security/csp-report logs apres deploy du frame-src fix
  • Marketplace public GET /api/registry/skills reste public : OK mais reviewer une fois qu'un quota est expose

Statut

AxeSeverityStatus
Auth wrappers — endpoints dashboardCritical✅ Shippe (commits 8dfebaf0, e15dd235)
CSP frame-src 'self'Critical✅ Shippe (43f8db29)
Cron timing-safeHigh✅ Shippe (d843022a)
withInternalAuth prod guardHigh✅ Shippe (8dfebaf0)
vercel.json CSP driftHigh✅ Shippe (8c9b3121)
Proxy preview BLOCKING_HEADERSMedium✅ Shippe (8c9b3121)
Org config Data CachePerf✅ Shippe (cd1cee37)
Vercel Firewall + BotIDMedium⏳ Plan ici (effort S)
Bibliotheque d'auth apres v4Medium⏳ Decision a prendre (ADR), pas une migration
Playwright tenant e2eHigh⏳ Plan ici (effort L)
revalidateTag marketplaceLow⏳ Deferre (effort M)