Session Fixation in Adonisjs with Firestore
Session Fixation in Adonisjs with Firestore — how this specific combination creates or exposes the vulnerability
Session fixation occurs when an application allows an attacker to force a user to use a known session identifier. In Adonisjs, the default session management behavior and the way session data is stored and validated interact with Firestore in ways that can enable fixation if certain protections are absent or misconfigured.
Adonisjs uses an extensible session layer. By default, it generates a new session ID upon authentication (regeneration), and stores session records server-side. When using Firestore as the session store, the session document ID typically maps to the session ID. If the application does not enforce regeneration after login, an attacker can set a victim’s session identifier (e.g., via a predictable token or by forcing a cookie value) and later use that same identifier to hijack the authenticated session.
With Firestore, session documents are read and written using the document ID as the key. If Adonisjs does not rotate the session ID on privilege change (e.g., post-login), the same Firestore document persists across authentication states. This creates a direct path for fixation: the attacker’s pre-set session ID remains valid after the victim logs in, because the backend does not issue a new ID or properly validate that the authenticated session’s ID matches the authenticated principal.
The risk is compounded when session data in Firestore includes metadata such as IP or user-agent, and the application performs incomplete validation. For example, if the session document is keyed only by the session ID and lacks binding to the user record or a freshness indicator, an attacker can reuse the document across requests. Firestore security rules do not mitigate application-layer session fixation; they only control document read/write access. Therefore, the application must ensure session ID rotation and strict ownership checks regardless of storage backend.
Real-world patterns include an attacker sending a link with a session cookie already set, or exploiting a login flow that preserves the pre-authentication session document. In Adonisjs, this can happen when session regeneration is omitted from the authentication controller, and when the Firestore adapter does not enforce strict document ownership checks tied to the user identity.
Firestore-Specific Remediation in Adonisjs — concrete code fixes
Remediation focuses on ensuring session ID rotation, binding sessions to user identity, and validating session provenance on each request. Below are concrete, idiomatic Adonisjs code examples using Firestore as the session store.
1. Enforce session regeneration on login
Always rotate the session after successful authentication to break any pre-set identifiers.
import { HttpContextContract } from '@ioc:Adonis/Core/HttpContext'
import Session from 'App/Services/SessionService'
export default class SessionsController {
public async login({ request, auth, session, response }: HttpContextContract) {
const { email, password } = request.only(['email', 'password'])
const user = await auth.authenticate()
// Rotate session ID to prevent fixation
await session.regenerate()
// Bind session to user identity in Firestore
await Session.bindUserToSession(user.id, session.id)
session.flash({ auth_status: 'Logged in' })
return response.redirect('/dashboard')
}
}
2. Firestore session store implementation with user binding
Implement a custom Firestore-backed session adapter that stores the user ID within the session document and validates ownership on retrieval.
import { FirestoreClient } from '@google-cloud/firestore'
import SessionContract from '@ioc:Adonis/Session/Session'
export default class FirestoreSessionStore {
private db: FirestoreClient
private collection = 'sessions'
constructor() {
this.db = new FirestoreClient()
}
public async create(sessionId: string, payload: string, expiresAt: Date) {
const docRef = this.db.collection(this.collection).doc(sessionId)
await docRef.set({
payload,
expires_at: expiresAt.toISOString(),
created_at: new Date().toISOString(),
})
}
public async update(sessionId: string, payload: string) {
const docRef = this.db.collection(this.collection).doc(sessionId)
await docRef.update({ payload, updated_at: new Date().toISOString() })
}
public async fetch(sessionId: string, user: { id: string }) {
const docRef = this.db.collection(this.collection).doc(sessionId)
const doc = await docRef.get()
if (!doc.exists) return null
const data = doc.data()
// Critical: validate that the session belongs to the authenticated user
if (data.user_id !== user.id) {
throw new Error('Session ownership mismatch')
}
return data.payload
}
public async delete(sessionId: string) {
await this.db.collection(this.collection).doc(sessionId).delete()
}
}
3. Middleware to validate session-user binding on each request
Add a lightweight middleware that ensures the session document in Firestore is tied to the authenticated user.
import { HttpContextContract } from '@ioc:Adonis/Core/HttpContext'
import FirestoreSessionStore from 'App/Services/FirestoreSessionStore'
const sessionStore = new FirestoreSessionStore()
export default class ValidateSessionBinding {
public async handle({ session, auth, response, next }: HttpContextContract) {
const user = auth.getUserOrFail()
const sessionData = await sessionStore.fetch(session.id, { id: user.id })
if (!sessionData) {
return response.unauthorized('Invalid session')
}
await next()
}
}
4. Configuration: disable predictable session IDs and enable secure defaults
Ensure Adonisjs is configured to use cryptographically strong session IDs and to bind sessions to additional context where feasible.
// config/session.ts
export default {
driver: 'firestore',
key: '__session',
secure: true,
httpOnly: true,
sameSite: 'lax',
// Ensure session IDs are long and random; avoid custom or predictable IDs
}
5. Continuous monitoring via GitHub Action
Use the middleBrick GitHub Action to add API security checks to your CI/CD pipeline and fail builds if risk scores drop below your threshold. This complements runtime protections by catching misconfigurations before deployment.
# .github/workflows/security.yml
name: API Security Checks
on: [push, pull_request]
jobs:
security:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Run middleBrick scan
uses: middlebjorn/middlebrick-github-action@v1
with:
url: 'https://your-api.example.com/openapi.json'
threshold: 'C'