Session Fixation in Adonisjs with Basic Auth
Session Fixation in Adonisjs with Basic Auth — how this specific combination creates or exposes the vulnerability
Session fixation in AdonisJS when Basic Auth is in use arises from a mismatch between how HTTP Basic Authentication credentials are validated and how session identifiers are assigned or accepted. In AdonisJS, Basic Auth is typically handled in an authentication layer that verifies a username and password on each request, often via an auth:basic route middleware or an Authenticator provider. When authentication succeeds, the application may create or attach a session (for example to remember post-login state) using AdonisJS session utilities such as auth.use('session'). If the session identifier (e.g., a cookie named sid) is accepted from the client without being regenerated after successful authentication, an attacker can fix the session ID before the victim authenticates.
Consider an endpoint protected by Basic Auth where the server always uses the client-supplied session ID (e.g., from request.cookies.sid) without forcing a new session after credentials are verified. An attacker can craft a link with a known session ID, get the victim to authenticate with valid Basic Auth credentials over HTTPS, and then use the known session ID to impersonate the victim. This works because Basic Auth transmits credentials on every request; if the session binding is weak or missing integrity checks, the fixed session becomes a privileged session for the attacker.
Additionally, if AdonisJS session configuration is permissive (e.g., not setting httpOnly, secure, or proper sameSite flags), and if the application does not tie the session explicitly to the authenticated identity (e.g., by validating the session user ID on each request), the fixed session can be abused across users or across authentication boundaries. The risk is higher in APIs that mix cookie-based sessions with header-based Basic Auth, where developers might assume the auth header alone suffices while inadvertently relying on a mutable session state.
Basic Auth-Specific Remediation in Adonisjs — concrete code fixes
To mitigate session fixation in AdonisJS with Basic Auth, ensure that a new session is created (or the session binding is strongly validated) immediately after successful authentication. Do not rely on the client-supplied session ID. Below are concrete code examples and configuration steps.
1. Force session regeneration after Basic Auth login
If you use session-based authentication alongside Basic Auth, regenerate the session ID after verifying credentials. This ensures the server-side session is not tied to a client-chosen identifier.
import { HttpContextContract } from '@ioc:Adonis/Core/HttpContext'
import { schema } from '@ioc:Adonis/Core/Validator'
export default class SessionsController {
public async authenticate({ request, auth, session }: HttpContextContract) {
const bodySchema = schema.create({
username: schema.string.optional(),
password: schema.string.optional()
})
const validated = await request.validate({ schema: bodySchema })
// Example: Basic Auth via custom header validation (not relying on client session ID)
const username = request.header('x-api-user')
const password = request.header('x-api-key')
// Perform your own verification (e.g., against a database or LDAP)
const isValid = await this.verifyBasicCredentials(username, password)
if (!isValid) {
return response.unauthorized({ message: 'Invalid credentials' })
}
// Regenerate session to prevent fixation
await session.regenerate()
// Bind identity to the new session explicitly
session.put('userId', userId)
session.put('authMethod', 'basic')
return { message: 'Authenticated', userId }
}
private async verifyBasicCredentials(username: string | undefined, password: string | undefined): Promise {
// Replace with your credential store lookup
return username === 'alice' && password === 's3cret'
}
}
2. Use Basic Auth without session reliance, or lock sessions to authentication context
If your API primarily uses Basic Auth statelessly, avoid creating sessions unless necessary. When sessions are required, bind them to the authenticated identity and validate on each request.
import { HttpContextContract } from '@ioc:Adonis/Core/HttpContext'
export default class AuthMiddleware {
public async handle(ctx: HttpContextContract, next: () => Promise) {
const username = ctx.request.header('authorization-user')
const token = ctx.request.header('authorization-token')
if (!username || !token) {
return ctx.response.unauthorized({ message: 'Missing auth headers' })
}
// Verify credentials (e.g., against hashed tokens)
const user = await User.findBy('username', username)
if (!user || user.token !== hash(token)) {
return ctx.response.unauthorized({ message: 'Invalid basic auth' })
}
// If using session, ensure it reflects the authenticated user and is not fixable
await ctx.session.regenerate()
ctx.session.put('userId', user.id)
await next()
}
}
3. Session configuration hardening
Adjust AdonisJS session settings to reduce exposure:
- Set
httpOnly: true,secure: true(in production), andsameSite: 'lax'(or'strict'for sensitive endpoints) inconfig/session.ts. - Do not accept session identifiers from query parameters or non-HTTP-safe methods.
- Ensure your application validates the session user ID on every request, even when Basic Auth headers are present, to avoid desync between auth and session state.
4. Validate identity on every request
Even when using Basic Auth headers, treat each request independently. Do not assume a pre-existing session alone proves authentication unless you have verified the session’s binding to the current credentials on every call.
// Example per-request validation
import { HttpContextContract } from '@ioc:Adonis/Core/HttpContext'
export async function ensureAuthenticated(ctx: HttpContextContract) {
const username = ctx.request.header('authorization-user')
const password = ctx.request.header('authorization-token')
const user = await User.findBy('username', username)
if (!user || user.password !== hash(password)) {
ctx.response.unauthorized({ message: 'Unauthorized' })
}
// Optionally ensure session matches user if sessions are used
if (ctx.session.get('userId') !== user.id) {
await ctx.session.regenerate()
ctx.session.put('userId', user.id)
}
}