security: fix CORS, CSRF, CSP, HSTS, sessions, and path traversal

- CORS: restrict origin from wildcard '*' to app URL from env (prevents
  cross-origin requests from arbitrary sites)
- Sessions: enable @adonisjs/session with cookie store and httpOnly/secure
  cookie flags; uncomment session middleware in kernel.ts
- CSRF: enable shield CSRF protection (requires sessions); uses XSRF-TOKEN
  cookie mechanism compatible with Inertia.js/Axios; exempts /api/health
  and SSE transmit endpoints
- CSP: enable Content Security Policy with restrictive directives
  (no object-src, no frame-src, self-only script/style/connect/font)
- HSTS: enable HTTP Strict Transport Security in production only
- Path traversal: tighten filenameParamValidator to block /, \, ..,
  and shell special characters; reduce max length from 4096 to 255
- env: add URL to .env.example; uncomment SESSION_DRIVER validation in env.ts

https://claude.ai/code/session_01WfRC4tDeYprykhMrg4PxX6
This commit is contained in:
Claude 2026-03-22 21:11:18 +00:00
parent f004c002a7
commit 3ddbe731a5
No known key found for this signature in database
7 changed files with 72 additions and 50 deletions

View File

@ -1,5 +1,6 @@
PORT=8080 PORT=8080
HOST=localhost HOST=localhost
URL=http://localhost:8080
LOG_LEVEL=info LOG_LEVEL=info
APP_KEY=some_random_key APP_KEY=some_random_key
NODE_ENV=development NODE_ENV=development

View File

@ -68,7 +68,14 @@ export const remoteDownloadValidatorOptional = vine.compile(
export const filenameParamValidator = vine.compile( export const filenameParamValidator = vine.compile(
vine.object({ vine.object({
params: vine.object({ params: vine.object({
filename: vine.string().trim().minLength(1).maxLength(4096), filename: vine
.string()
.trim()
.minLength(1)
.maxLength(255)
.regex(/^[^/\\]*$/) // Disallow path separators to prevent traversal
.regex(/^(?!\.\.)/) // Disallow leading double-dots
.regex(/^[^<>:"|?*\x00-\x1f]+$/), // Disallow shell/filesystem special chars
}), }),
}) })
) )

View File

@ -1,3 +1,4 @@
import env from '#start/env'
import { defineConfig } from '@adonisjs/cors' import { defineConfig } from '@adonisjs/cors'
/** /**
@ -8,7 +9,7 @@ import { defineConfig } from '@adonisjs/cors'
*/ */
const corsConfig = defineConfig({ const corsConfig = defineConfig({
enabled: true, enabled: true,
origin: ['*'], origin: [env.get('URL')],
methods: ['GET', 'HEAD', 'POST', 'PUT', 'DELETE'], methods: ['GET', 'HEAD', 'POST', 'PUT', 'DELETE'],
headers: true, headers: true,
exposeHeaders: [], exposeHeaders: [],

View File

@ -1,48 +1,48 @@
// import env from '#start/env' import env from '#start/env'
// import app from '@adonisjs/core/services/app' import app from '@adonisjs/core/services/app'
// import { defineConfig, stores } from '@adonisjs/session' import { defineConfig, stores } from '@adonisjs/session'
// const sessionConfig = defineConfig({ const sessionConfig = defineConfig({
// enabled: false, enabled: true,
// cookieName: 'adonis-session', cookieName: 'nomad-session',
// /** /**
// * When set to true, the session id cookie will be deleted * When set to true, the session id cookie will be deleted
// * once the user closes the browser. * once the user closes the browser.
// */ */
// clearWithBrowser: false, clearWithBrowser: false,
// /** /**
// * Define how long to keep the session data alive without * Define how long to keep the session data alive without
// * any activity. * any activity.
// */ */
// age: '2h', age: '2h',
// /** /**
// * Configuration for session cookie and the * Configuration for session cookie and the
// * cookie store * cookie store
// */ */
// cookie: { cookie: {
// path: '/', path: '/',
// httpOnly: true, httpOnly: true,
// secure: app.inProduction, secure: app.inProduction,
// sameSite: 'lax', sameSite: 'lax',
// }, },
// /** /**
// * The store to use. Make sure to validate the environment * The store to use. Make sure to validate the environment
// * variable in order to infer the store name without any * variable in order to infer the store name without any
// * errors. * errors.
// */ */
// store: env.get('SESSION_DRIVER'), store: env.get('SESSION_DRIVER'),
// /** /**
// * List of configured stores. Refer documentation to see * List of configured stores. Refer documentation to see
// * list of available stores and their config. * list of available stores and their config.
// */ */
// stores: { stores: {
// cookie: stores.cookie(), cookie: stores.cookie(),
// }, },
// }) })
// export default sessionConfig export default sessionConfig

View File

@ -1,3 +1,4 @@
import app from '@adonisjs/core/services/app'
import { defineConfig } from '@adonisjs/shield' import { defineConfig } from '@adonisjs/shield'
const shieldConfig = defineConfig({ const shieldConfig = defineConfig({
@ -6,8 +7,19 @@ const shieldConfig = defineConfig({
* to learn more * to learn more
*/ */
csp: { csp: {
enabled: false, enabled: true,
directives: {}, directives: {
defaultSrc: ["'self'"],
scriptSrc: ["'self'", "'unsafe-inline'"], // unsafe-inline required for Inertia.js page props
styleSrc: ["'self'", "'unsafe-inline'"],
imgSrc: ["'self'", 'data:', 'blob:'],
connectSrc: ["'self'"],
fontSrc: ["'self'"],
objectSrc: ["'none'"],
frameSrc: ["'none'"],
baseUri: ["'self'"],
formAction: ["'self'"],
},
reportOnly: false, reportOnly: false,
}, },
@ -16,8 +28,9 @@ const shieldConfig = defineConfig({
* to learn more * to learn more
*/ */
csrf: { csrf: {
enabled: false, // TODO: Enable CSRF protection enabled: true,
exceptRoutes: [], // Exempt health check and SSE/transmit endpoints from CSRF
exceptRoutes: ['/api/health', '/__transmit/events', '/__transmit/unsubscribe'],
enableXsrfCookie: true, enableXsrfCookie: true,
methods: ['POST', 'PUT', 'PATCH', 'DELETE'], methods: ['POST', 'PUT', 'PATCH', 'DELETE'],
}, },
@ -35,7 +48,7 @@ const shieldConfig = defineConfig({
* Force browser to always use HTTPS * Force browser to always use HTTPS
*/ */
hsts: { hsts: {
enabled: false, // TODO: Enable HSTS in production enabled: app.inProduction,
maxAge: '180 days', maxAge: '180 days',
}, },

View File

@ -32,7 +32,7 @@ export default await Env.create(new URL('../', import.meta.url), {
| Variables for configuring session package | Variables for configuring session package
|---------------------------------------------------------- |----------------------------------------------------------
*/ */
//SESSION_DRIVER: Env.schema.enum(['cookie', 'memory'] as const), SESSION_DRIVER: Env.schema.enum(['cookie', 'memory'] as const),
/* /*
|---------------------------------------------------------- |----------------------------------------------------------

View File

@ -37,7 +37,7 @@ server.use([
*/ */
router.use([ router.use([
() => import('@adonisjs/core/bodyparser_middleware'), () => import('@adonisjs/core/bodyparser_middleware'),
// () => import('@adonisjs/session/session_middleware'), () => import('@adonisjs/session/session_middleware'),
() => import('@adonisjs/shield/shield_middleware'), () => import('@adonisjs/shield/shield_middleware'),
]) ])