CWE-614: Sensitive Cookie Without 'Secure' Flag - JavaScript/Node.js
Overview
Sensitive Cookie Without 'Secure' Flag in JavaScript/Node.js applications occurs when cookies containing sensitive data (session IDs, authentication tokens, CSRF tokens, user identifiers) are set without the secure attribute. This allows cookies to be transmitted over unencrypted HTTP connections, exposing them to man-in-the-middle (MITM) attacks, network sniffing, session hijacking, and account compromise.
Common JavaScript Vulnerability Scenarios:
- Express cookies without
secure: true - Fastify cookies missing
secureattribute - Next.js API routes with insecure cookies
- Custom session management without proper flags
- OAuth tokens in insecure cookies
- Remember-me cookies transmitted over HTTP
JavaScript/Node.js Framework Cookie Security:
- Express:
res.cookie('name', 'value', { secure: true, httpOnly: true, sameSite: 'strict' }) - Fastify:
reply.setCookie('name', 'value', { secure: true, httpOnly: true, sameSite: 'strict' }) - Next.js:
res.setHeader('Set-Cookie', serialize('name', 'value', { secure: true, httpOnly: true, sameSite: 'strict' })) - NestJS:
@Res({ passthrough: true }) res: Response; res.cookie('name', 'value', { secure: true, httpOnly: true, sameSite: 'strict' })
Primary Defence: Set secure: true, httpOnly: true, and sameSite: 'strict' on all cookies containing sensitive data, and enforce HTTPS in production.
Common Vulnerable Patterns
Express Cookie Without Secure Flag
// VULNERABLE - Session cookie without secure flag
const express = require('express');
const crypto = require('crypto');
const app = express();
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
app.post('/login', (req, res) => {
const { username, password } = req.body;
if (authenticateUser(username, password)) {
const sessionToken = crypto.randomBytes(32).toString('base64url');
// VULNERABLE - Missing secure: true
res.cookie('session_id', sessionToken, {
httpOnly: true, // Good, but not enough
maxAge: 3600000 // 1 hour
});
res.json({ status: 'logged_in' });
} else {
res.status(401).json({ error: 'Invalid credentials' });
}
});
function authenticateUser(username, password) {
// Authentication logic
return true;
}
app.listen(3000, () => {
console.log('Server running on port 3000');
});
Why this is vulnerable:
- Cookie transmitted over HTTP
- Susceptible to MITM attacks
- Network sniffing can capture session token
- Session hijacking possible
Express Session Without Secure Configuration
// VULNERABLE - express-session without secure cookies
const express = require('express');
const session = require('express-session');
const crypto = require('crypto');
const app = express();
// VULNERABLE - Session configuration missing secure flag
app.use(session({
secret: crypto.randomBytes(64).toString('hex'),
resave: false,
saveUninitialized: false,
cookie: {
httpOnly: true,
maxAge: 24 * 60 * 60 * 1000 // 24 hours
// Missing: secure: true
// Missing: sameSite: 'strict'
}
}));
app.post('/login', (req, res) => {
const { username, password } = req.body;
if (authenticateUser(username, password)) {
// VULNERABLE - Session cookie sent without secure flag
req.session.userId = getUserId(username);
req.session.username = username;
res.json({ status: 'logged_in' });
} else {
res.status(401).json({ error: 'Invalid credentials' });
}
});
function authenticateUser(username, password) {
return true;
}
function getUserId(username) {
return 123;
}
app.listen(3000);
Why this is vulnerable:
- express-session cookies not secure
- Session data exposed over HTTP
- Production misconfiguration
- CSRF also possible without sameSite
Fastify Cookie Without Secure Flag
// VULNERABLE - Fastify application with insecure cookies
const fastify = require('fastify')({ logger: true });
const fastifyCookie = require('@fastify/cookie');
const crypto = require('crypto');
fastify.register(fastifyCookie);
fastify.post('/login', async (request, reply) => {
const { username, password } = request.body;
if (authenticateUser(username, password)) {
const sessionToken = crypto.randomBytes(32).toString('base64url');
// VULNERABLE - Missing secure option
reply.setCookie('session_id', sessionToken, {
httpOnly: true,
maxAge: 3600, // 1 hour in seconds
path: '/'
// Missing: secure: true
// Missing: sameSite: 'strict'
});
return { status: 'logged_in' };
}
reply.code(401);
return { error: 'Invalid credentials' };
});
function authenticateUser(username, password) {
return true;
}
fastify.listen({ port: 3000 }, (err) => {
if (err) throw err;
});
Why this is vulnerable:
- Fastify cookies missing secure flag
- Cookie sent over HTTP
- Authentication token exposed
- Production deployment risk
Next.js API Route Without Secure Cookie
// VULNERABLE - Next.js API route with insecure cookies
// pages/api/login.js
import { serialize } from 'cookie';
import crypto from 'crypto';
export default async function handler(req, res) {
if (req.method === 'POST') {
const { username, password } = req.body;
if (await authenticateUser(username, password)) {
const sessionToken = crypto.randomBytes(32).toString('base64url');
// VULNERABLE - Cookie without secure flag
const cookie = serialize('session_token', sessionToken, {
httpOnly: true,
maxAge: 3600,
path: '/'
// Missing: secure: true
// Missing: sameSite: 'strict'
});
res.setHeader('Set-Cookie', cookie);
res.status(200).json({ status: 'logged_in' });
} else {
res.status(401).json({ error: 'Invalid credentials' });
}
} else {
res.status(405).json({ error: 'Method not allowed' });
}
}
async function authenticateUser(username, password) {
return true;
}
Why this is vulnerable:
- Next.js cookies not secure
- Cookie serialization incomplete
- Missing critical security flags
- SSR/API route misconfiguration
Remember-Me Cookie Without Secure Flag
// VULNERABLE - Remember-me functionality with insecure cookie
const express = require('express');
const crypto = require('crypto');
const app = express();
app.use(express.json());
app.post('/login', async (req, res) => {
const { username, password, rememberMe } = req.body;
if (await authenticateUser(username, password)) {
const sessionToken = crypto.randomBytes(32).toString('base64url');
// Session cookie (also vulnerable but short-lived)
res.cookie('session_id', sessionToken, {
httpOnly: true,
maxAge: 3600000 // 1 hour
// Missing: secure: true
});
if (rememberMe) {
const rememberToken = crypto.randomBytes(64).toString('base64url');
// VULNERABLE - Long-lived remember-me cookie without secure flag
res.cookie('remember_me', rememberToken, {
httpOnly: true,
maxAge: 30 * 24 * 3600000 // 30 days - VERY vulnerable!
// Missing: secure: true
// Missing: sameSite: 'strict'
});
// Store token in database
await storeRememberToken(username, rememberToken);
}
res.json({ status: 'logged_in' });
} else {
res.status(401).json({ error: 'Invalid credentials' });
}
});
async function authenticateUser(username, password) {
return true;
}
async function storeRememberToken(username, token) {
// Database storage
}
app.listen(3000);
Why this is vulnerable:
- Long-lived cookies especially dangerous
- 30-day exposure window
- No secure flag = persistent vulnerability
- Enables long-term session hijacking
JWT Cookie Without Proper Security
// VULNERABLE - JWT stored in cookie without security flags
const express = require('express');
const jwt = require('jsonwebtoken');
const app = express();
app.use(express.json());
const JWT_SECRET = process.env.JWT_SECRET || 'default-secret';
app.post('/api/login', (req, res) => {
const { username, password } = req.body;
if (authenticateUser(username, password)) {
const token = jwt.sign(
{ username, iat: Date.now() },
JWT_SECRET,
{ expiresIn: '1h' }
);
// VULNERABLE - Multiple issues
res.cookie('auth_token', token, {
maxAge: 3600000
// Missing: secure: true
// Missing: httpOnly: true
// Missing: sameSite: 'strict'
});
res.json({ status: 'success' });
} else {
res.status(401).json({ error: 'Authentication failed' });
}
});
function authenticateUser(username, password) {
return true;
}
app.listen(3000);
Why this is vulnerable:
- No
secure: trueflag - No
httpOnly: true(XSS vulnerable) - No
sameSiteattribute (CSRF vulnerable) - Triple vulnerability in one cookie
OAuth State Cookie Without Security
// VULNERABLE - OAuth state cookie without security flags
const express = require('express');
const crypto = require('crypto');
const app = express();
const OAUTH_CLIENT_ID = process.env.OAUTH_CLIENT_ID;
app.get('/oauth/authorize', (req, res) => {
const state = crypto.randomBytes(32).toString('base64url');
// VULNERABLE - OAuth state cookie without secure flag
res.cookie('oauth_state', state, {
maxAge: 600000 // 10 minutes
// Missing: secure: true
// Missing: httpOnly: true
// Missing: sameSite: 'lax' (for OAuth redirects)
});
const oauthUrl = `https://oauth.provider.com/authorize?` +
`client_id=${OAUTH_CLIENT_ID}&` +
`redirect_uri=https://example.com/oauth/callback&` +
`state=${state}`;
res.redirect(oauthUrl);
});
app.get('/oauth/callback', (req, res) => {
const stateParam = req.query.state;
const stateCookie = req.cookies.oauth_state;
if (stateParam !== stateCookie) {
return res.status(400).json({ error: 'Invalid state' });
}
// Continue OAuth flow
res.json({ status: 'success' });
});
app.listen(3000);
Why this is vulnerable:
- OAuth state token exposed over HTTP
- CSRF protection compromised
- Enables OAuth token theft
- OAuth security model broken
NestJS Cookie Without Security Flags
// VULNERABLE - NestJS application with insecure cookies
import { Controller, Post, Body, Res } from '@nestjs/common';
import { Response } from 'express';
import * as crypto from 'crypto';
@Controller('api/auth')
export class AuthController {
@Post('login')
async login(
@Body() body: { username: string, password: string },
@Res({ passthrough: true }) res: Response
) {
if (await this.authenticateUser(body.username, body.password)) {
const sessionToken = crypto.randomBytes(32).toString('base64url');
// VULNERABLE - Cookie without security flags
res.cookie('session_token', sessionToken, {
httpOnly: true,
maxAge: 3600000 // 1 hour
// Missing: secure: true
// Missing: sameSite: 'strict'
});
return { status: 'logged_in' };
}
throw new Error('Invalid credentials');
}
private async authenticateUser(username: string, password: string): Promise<boolean> {
return true;
}
}
Why this is vulnerable:
- NestJS cookies missing secure flag
- Modern framework still requires explicit security
- Production deployment risk
- Session hijacking possible
Secure Patterns
Express Cookie With All Security Flags
// SECURE - Express cookie with proper security flags
const express = require('express');
const crypto = require('crypto');
const https = require('https');
const fs = require('fs');
const app = express();
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
// SECURE - Trust proxy if behind reverse proxy (Nginx, etc.)
app.set('trust proxy', 1);
app.post('/login', (req, res) => {
const { username, password } = req.body;
if (authenticateUser(username, password)) {
const sessionToken = crypto.randomBytes(32).toString('base64url');
// SECURE - All critical security flags set
res.cookie('session_id', sessionToken, {
secure: true, // HTTPS only
httpOnly: true, // Not accessible via JavaScript
sameSite: 'strict', // CSRF protection
maxAge: 3600000, // 1 hour
path: '/'
});
res.json({ status: 'logged_in' });
} else {
res.status(401).json({ error: 'Invalid credentials' });
}
});
app.post('/logout', (req, res) => {
// SECURE - Clear cookie with same settings
res.clearCookie('session_id', {
secure: true,
httpOnly: true,
sameSite: 'strict',
path: '/'
});
res.json({ status: 'logged_out' });
});
function authenticateUser(username, password) {
// Secure authentication logic
return true;
}
// SECURE - Run with HTTPS in production
if (process.env.NODE_ENV === 'production') {
const httpsOptions = {
key: fs.readFileSync(process.env.SSL_KEY_PATH),
cert: fs.readFileSync(process.env.SSL_CERT_PATH)
};
https.createServer(httpsOptions, app).listen(443, () => {
console.log('HTTPS Server running on port 443');
});
} else {
// Development only
app.listen(3000, () => {
console.log('Development server on port 3000');
});
}
Why this works:
- Secure + HttpOnly + SameSite prevent HTTP leakage, XSS access, and CSRF.
- HTTPS +
trust proxyensure secure cookies work behind TLS termination. - Short lifetimes and proper logout reduce exposure.
Express Session With Secure Configuration
// SECURE - express-session with proper security
const express = require('express');
const session = require('express-session');
const RedisStore = require('connect-redis').default;
const { createClient } = require('redis');
const crypto = require('crypto');
const app = express();
app.use(express.json());
// SECURE - Trust proxy for secure cookies behind reverse proxy
app.set('trust proxy', 1);
// Create Redis client for session storage
const redisClient = createClient({
url: process.env.REDIS_URL || 'redis://localhost:6379'
});
redisClient.connect().catch(console.error);
// SECURE - Session configuration with all security flags
app.use(session({
store: new RedisStore({ client: redisClient }),
secret: process.env.SESSION_SECRET || crypto.randomBytes(64).toString('hex'),
resave: false,
saveUninitialized: false,
name: 'sessionId', // Custom name (don't use default 'connect.sid')
cookie: {
secure: true, // HTTPS only
httpOnly: true, // Not accessible via JavaScript
sameSite: 'strict', // CSRF protection
maxAge: 3600000 // 1 hour
}
}));
app.post('/login', (req, res) => {
const { username, password } = req.body;
if (authenticateUser(username, password)) {
// Regenerate session ID to prevent session fixation
req.session.regenerate((err) => {
if (err) {
return res.status(500).json({ error: 'Session error' });
}
// SECURE - Session cookie automatically uses secure settings
req.session.userId = getUserId(username);
req.session.username = username;
res.json({ status: 'logged_in' });
});
} else {
res.status(401).json({ error: 'Invalid credentials' });
}
});
app.post('/logout', (req, res) => {
req.session.destroy((err) => {
if (err) {
return res.status(500).json({ error: 'Logout failed' });
}
res.clearCookie('sessionId', {
secure: true,
httpOnly: true,
sameSite: 'strict'
});
res.json({ status: 'logged_out' });
});
});
function authenticateUser(username, password) {
return true;
}
function getUserId(username) {
return 123;
}
// SECURE - HTTPS server
const https = require('https');
const fs = require('fs');
if (process.env.NODE_ENV === 'production') {
const httpsOptions = {
key: fs.readFileSync(process.env.SSL_KEY_PATH),
cert: fs.readFileSync(process.env.SSL_CERT_PATH)
};
https.createServer(httpsOptions, app).listen(443);
}
Why this works:
- Secure cookies + server-side sessions keep data off the client.
- Regenerating session IDs blocks fixation attacks.
- Strong secrets, sane expiry, and logout cleanup limit exposure.
Fastify With Secure Cookies
// SECURE - Fastify with proper cookie security
const fastify = require('fastify')({
logger: true,
https: {
key: require('fs').readFileSync(process.env.SSL_KEY_PATH),
cert: require('fs').readFileSync(process.env.SSL_CERT_PATH)
}
});
const fastifyCookie = require('@fastify/cookie');
const fastifySession = require('@fastify/session');
const crypto = require('crypto');
fastify.register(fastifyCookie);
// SECURE - Session with all security flags
fastify.register(fastifySession, {
secret: process.env.SESSION_SECRET || crypto.randomBytes(64).toString('hex'),
cookieName: 'sessionId',
cookie: {
secure: true, // HTTPS only
httpOnly: true, // Not accessible via JavaScript
sameSite: 'strict', // CSRF protection
maxAge: 3600000 // 1 hour
},
saveUninitialized: false
});
fastify.post('/login', async (request, reply) => {
const { username, password } = request.body;
if (authenticateUser(username, password)) {
const sessionToken = crypto.randomBytes(32).toString('base64url');
// SECURE - Cookie with all security flags
reply.setCookie('session_id', sessionToken, {
secure: true, // HTTPS only
httpOnly: true, // Not accessible via JavaScript
sameSite: 'strict', // CSRF protection
maxAge: 3600, // 1 hour (in seconds)
path: '/'
});
// Also set session data
request.session.set('userId', getUserId(username));
request.session.set('username', username);
return { status: 'logged_in' };
}
reply.code(401);
return { error: 'Invalid credentials' };
});
fastify.post('/logout', async (request, reply) => {
// Clear cookie
reply.clearCookie('session_id', {
secure: true,
httpOnly: true,
sameSite: 'strict',
path: '/'
});
// Destroy session
request.session.destroy();
return { status: 'logged_out' };
});
function authenticateUser(username, password) {
return true;
}
function getUserId(username) {
return 123;
}
// SECURE - Listen on HTTPS
fastify.listen({ port: 443, host: '0.0.0.0' }, (err) => {
if (err) throw err;
});
Why this works:
- HTTPS-only server + secure cookies prevent HTTP leakage.
- Manual and session cookies share Secure/HttpOnly/SameSite flags.
- Session hygiene (no empty sessions, logout cleanup) reduces exposure.
Next.js API Route With Secure Cookies
// SECURE - Next.js API route with proper cookie security
// pages/api/login.js
import { serialize } from 'cookie';
import crypto from 'crypto';
export default async function handler(req, res) {
if (req.method === 'POST') {
const { username, password } = req.body;
if (await authenticateUser(username, password)) {
const sessionToken = crypto.randomBytes(32).toString('base64url');
// SECURE - Cookie with all security flags
const cookie = serialize('session_token', sessionToken, {
secure: true, // HTTPS only
httpOnly: true, // Not accessible via JavaScript
sameSite: 'strict', // CSRF protection
maxAge: 3600, // 1 hour
path: '/'
});
res.setHeader('Set-Cookie', cookie);
res.status(200).json({ status: 'logged_in' });
} else {
res.status(401).json({ error: 'Invalid credentials' });
}
} else {
res.status(405).json({ error: 'Method not allowed' });
}
}
async function authenticateUser(username, password) {
// Secure authentication
return true;
}
// pages/api/logout.js
import { serialize } from 'cookie';
export default async function handler(req, res) {
if (req.method === 'POST') {
// SECURE - Delete cookie with same security settings
const cookie = serialize('session_token', '', {
secure: true,
httpOnly: true,
sameSite: 'strict',
maxAge: 0,
path: '/'
});
res.setHeader('Set-Cookie', cookie);
res.status(200).json({ status: 'logged_out' });
} else {
res.status(405).json({ error: 'Method not allowed' });
}
}
// next.config.js - SECURE: Force HTTPS in production
module.exports = {
async headers() {
return [
{
source: '/:path*',
headers: [
{
key: 'Strict-Transport-Security',
value: 'max-age=63072000; includeSubDomains; preload'
}
]
}
];
}
};
Why this works:
- Secure/HttpOnly/SameSite flags are applied at the header level.
- Logout clears cookies with matching attributes.
- HSTS enforces HTTPS for all routes and subdomains.
Secure Remember-Me Implementation
// SECURE - Remember-me with proper security
const express = require('express');
const crypto = require('crypto');
const bcrypt = require('bcrypt');
const app = express();
app.use(express.json());
app.set('trust proxy', 1);
class RememberMeService {
static generateToken() {
return crypto.randomBytes(64).toString('base64url');
}
static async hashToken(token) {
return await bcrypt.hash(token, 12);
}
static async storeToken(username, token) {
const tokenHash = await this.hashToken(token);
// Store in database with expiration
await db.rememberTokens.insert({
username,
tokenHash,
createdAt: new Date(),
expiresAt: new Date(Date.now() + 30 * 24 * 3600000)
});
}
static async verifyToken(token) {
const tokens = await db.rememberTokens.find({
expiresAt: { $gt: new Date() }
});
for (const record of tokens) {
if (await bcrypt.compare(token, record.tokenHash)) {
return record.username;
}
}
return null;
}
}
app.post('/login', async (req, res) => {
const { username, password, rememberMe } = req.body;
if (await authenticateUser(username, password)) {
const sessionToken = crypto.randomBytes(32).toString('base64url');
// SECURE - Session cookie with all flags
res.cookie('session_id', sessionToken, {
secure: true,
httpOnly: true,
sameSite: 'strict',
maxAge: 3600000 // 1 hour
});
if (rememberMe) {
const rememberToken = RememberMeService.generateToken();
await RememberMeService.storeToken(username, rememberToken);
// SECURE - Remember-me cookie with all flags
res.cookie('remember_me', rememberToken, {
secure: true, // HTTPS only
httpOnly: true, // Not accessible via JavaScript
sameSite: 'strict', // CSRF protection
maxAge: 30 * 24 * 3600000 // 30 days
});
}
res.json({ status: 'logged_in' });
} else {
res.status(401).json({ error: 'Invalid credentials' });
}
});
app.post('/auto-login', async (req, res) => {
const rememberToken = req.cookies.remember_me;
if (!rememberToken) {
return res.status(401).json({ error: 'No remember-me token' });
}
const username = await RememberMeService.verifyToken(rememberToken);
if (username) {
const sessionToken = crypto.randomBytes(32).toString('base64url');
// SECURE - Create new session
res.cookie('session_id', sessionToken, {
secure: true,
httpOnly: true,
sameSite: 'strict',
maxAge: 3600000
});
res.json({ status: 'auto_logged_in', username });
} else {
res.status(401).json({ error: 'Invalid token' });
}
});
async function authenticateUser(username, password) {
return true;
}
// Mock database
const db = {
rememberTokens: {
insert: async (doc) => {},
find: async (query) => []
}
};
module.exports = app;
Why this works:
- Secure/HttpOnly/SameSite protect both session and remember-me cookies.
- Tokens are hashed at rest, reducing impact of database compromise.
- Auto-login issues a fresh short-lived session cookie.
NestJS With Secure Cookie Configuration
// SECURE - NestJS application with proper cookie security
import { Controller, Post, Body, Res } from '@nestjs/common';
import { Response } from 'express';
import * as crypto from 'crypto';
@Controller('api/auth')
export class SecureAuthController {
@Post('login')
async login(
@Body() body: { username: string, password: string },
@Res({ passthrough: true }) res: Response
) {
if (await this.authenticateUser(body.username, body.password)) {
const sessionToken = crypto.randomBytes(32).toString('base64url');
// SECURE - Cookie with all security flags
res.cookie('session_token', sessionToken, {
secure: true, // HTTPS only
httpOnly: true, // Not accessible via JavaScript
sameSite: 'strict', // CSRF protection
maxAge: 3600000 // 1 hour
});
return { status: 'logged_in' };
}
throw new Error('Invalid credentials');
}
@Post('logout')
async logout(@Res({ passthrough: true }) res: Response) {
res.clearCookie('session_token', {
secure: true,
httpOnly: true,
sameSite: 'strict'
});
return { status: 'logged_out' };
}
private async authenticateUser(username: string, password: string): Promise<boolean> {
return true;
}
}
// main.ts - SECURE: NestJS HTTPS configuration
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import * as fs from 'fs';
async function bootstrap() {
const httpsOptions = {
key: fs.readFileSync(process.env.SSL_KEY_PATH),
cert: fs.readFileSync(process.env.SSL_CERT_PATH)
};
const app = await NestFactory.create(AppModule, {
httpsOptions
});
// Enable trust proxy for secure cookies
app.set('trust proxy', 1);
await app.listen(443);
}
bootstrap();
Why this works:
- HTTPS + Secure cookies prevent HTTP leakage.
- HttpOnly + SameSite protect against XSS and CSRF.
- Trust proxy + logout cleanup enforce correct lifecycle.
Verification
After implementing the recommended secure patterns, verify the fix through multiple approaches:
- Manual testing: Submit malicious payloads relevant to this vulnerability and confirm they're handled safely without executing unintended operations
- Code review: Confirm all instances use the secure pattern (parameterized queries, safe APIs, proper encoding) with no string concatenation or unsafe operations
- Static analysis: Use security scanners to verify no new vulnerabilities exist and the original finding is resolved
- Regression testing: Ensure legitimate user inputs and application workflows continue to function correctly
- Edge case validation: Test with special characters, boundary conditions, and unusual inputs to verify proper handling
- Framework verification: If using a framework or library, confirm the recommended APIs are used correctly according to documentation
- Authentication/session testing: Verify security controls remain effective and cannot be bypassed (if applicable to the vulnerability type)
- Rescan: Run the security scanner again to confirm the finding is resolved and no new issues were introduced