vibe-cookie-missing-flags
Pack: vibe-frontend Default severity: MAJOR Languages: TypeScript, JavaScript Lifecycle: beta Confidence: 0.8
What it catches
Session middleware / cookie config that's missing httpOnly: true, secure: true, or sameSite: 'lax'|'strict'. AI-generated auth code routinely uses a session library with default options that turn ALL three of these off.
Recognises patterns:
express-session—session({ ... })iron-session—sealData/ Iron config /getIronSession(req, res, { cookieOptions: {...} })cookie-session—cookieSession({ ... })next-auth(v4+) —cookies: { sessionToken: { options: { ... } } }in NextAuth config- Direct cookie setting —
res.cookie('name', val, { ... })
For each session/cookie config, checks whether all three flags are present:
httpOnly: true← prevents XSS-to-session-tokensecure: true← prevents HTTP downgrade sniffsameSite: 'lax'|'strict'← prevents CSRF
Why this matters for vibe-coded apps
Default session cookies in express-session, iron-session, cookie-session, and Next.js are sent over HTTP without httpOnly + secure + sameSite unless the config explicitly sets each. AI-generated apps almost never bother. Result:
- Missing `httpOnly`: Any XSS in the app becomes a session-token exfiltration.
- Missing `secure`: Any HTTP fallback (dev misconfiguration, MITM) becomes a session-token sniff.
- Missing `sameSite`: Any third-party POST becomes a Cross-Site Request Forgery (CSRF).
This is the #1 session-handling flaw in vibe-coded apps.
Example — flagged
// express-session with defaults (all three flags missing).
app.use(
session({
secret: 'your-secret-here',
resave: false,
saveUninitialized: true,
// MAJOR: missing httpOnly, secure, sameSite
})
);// iron-session without secure flags.
import { getIronSession } from 'iron-session';
export async function GET(req: NextRequest, res: NextResponse) {
// MAJOR: no cookieOptions with security flags.
const session = await getIronSession(req, res, {
cookieName: 'auth',
password: 'complex_password_at_least_32_chars_long',
});
return res.json(session);
}// next-auth without full cookie config.
export const authConfig = {
providers: [...],
cookies: {
sessionToken: {
// MAJOR: options object missing httpOnly, secure, sameSite.
options: {
path: '/',
httpOnly: false, // WRONG
},
},
},
};Example — not flagged
// express-session with all flags.
app.use(
session({
secret: 'your-secret-here',
resave: false,
saveUninitialized: true,
cookie: {
httpOnly: true, // ← OK: prevents XSS
secure: process.env.NODE_ENV === 'production', // ← OK: enables HTTPS only
sameSite: 'lax', // ← OK: prevents CSRF
},
})
);// iron-session with secure options.
const session = await getIronSession(req, res, {
cookieName: 'auth',
password: 'complex_password_at_least_32_chars_long',
cookieOptions: {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
sameSite: 'lax',
},
});// next-auth with full security config.
export const authConfig = {
providers: [...],
cookies: {
sessionToken: {
name: 'next-auth.session-token',
options: {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
sameSite: 'lax',
path: '/',
},
},
},
};Suggested fix
Add the missing flags to the cookie options object:
express-session:
app.use(
session({
secret: 'your-secret-here',
resave: false,
saveUninitialized: true,
cookie: {
httpOnly: true, // Prevent XSS
secure: process.env.NODE_ENV === 'production', // HTTPS only (dev exception)
sameSite: 'lax', // CSRF protection
maxAge: 24 * 60 * 60 * 1000, // 24 hours
},
})
);iron-session:
const session = await getIronSession(req, res, {
cookieName: 'auth',
password: process.env.SECRET_KEY,
cookieOptions: {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
sameSite: 'lax',
},
});next-auth:
export const authConfig = {
providers: [...],
callbacks: {...},
cookies: {
sessionToken: {
name: 'next-auth.session-token',
options: {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
sameSite: 'lax',
path: '/',
},
},
},
};Direct cookie setting:
res.cookie('sessionId', sessionToken, {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
sameSite: 'lax',
maxAge: 24 * 60 * 60 * 1000, // 24 hours
});Suppressing
// Reason: this is a non-sensitive tracking cookie, not a session token.
// codemore-ignore-next-line: vibe-cookie-missing-flags
res.cookie('analytics_id', uuid(), {
maxAge: 365 * 24 * 60 * 60 * 1000, // 1 year
});The directive must be on the line immediately before the target. If you put a comment between them, the directive suppresses the comment instead.
References
Implementation
Regex scan for session middleware / cookie config patterns. For each match, extracts the options object body and checks for the presence of httpOnly: true, secure: true (or a NODE_ENV conditional), and sameSite: 'lax'|'strict'. Missing flags are reported with a count of what's missing.
Source: `shared/packs/vibe-frontend/vibe-cookie-missing-flags.ts` Fixtures: `corpus/rules/vibe-cookie-missing-flags/`