Crlf Injection in Adonisjs with Firestore
Crlf Injection in Adonisjs with Firestore — how this specific combination creates or exposes the vulnerability
Crlf Injection occurs when an attacker can inject carriage return (CR, \r) and line feed (\n) characters into an HTTP header or a log entry, causing header splitting or log forging. In an AdonisJS application that integrates with Google Cloud Firestore, the risk arises when user-controlled input flows into functions that write to Firestore documents and later into response headers or server-side logs.
AdonisJS does not inherently sanitize data before it is used in outbound requests or log statements. If a developer passes unsanitized request parameters into Firestore operations and then uses parts of that data (e.g., a username or a document ID) in headers such as Location or X-Request-ID, an attacker can inject %0d%0a (or raw \r\n) sequences. For example, a document ID like abc\r\nX-Header: injected could corrupt a subsequent redirect or response header when the application formats logs or constructs HTTP replies.
When Firestore returns data that is then written to logs or echoed in HTTP responses—such as in an admin dashboard that streams document contents—unsanitized newlines can enable log injection or response splitting. This can obscure real events, forge audit trails, or facilitate HTTP response splitting attacks, which may allow an attacker to inject arbitrary headers or even a second HTTP response. Because Firestore stores and retrieves data as structured JSON, newline characters can be preserved in string fields, turning what appears to be safe storage into a transport or logging vector when AdonisJS mishandles the output.
Another angle is the use of Firestore document IDs or fields in server-side redirects or URL generation within AdonisJS routes. If an ID such as users/123\r\nSet-Cookie: token=evil is used to build a Location header, the injected CRLF can split the header and inject a new header. Even if Firestore itself does not interpret CRLF, the surrounding AdonisJS request/response handling does, making the combination dangerous when input validation is absent.
Firestore-Specific Remediation in Adonisjs — concrete code fixes
Remediation focuses on strict input validation, output encoding, and isolating Firestore data from header construction and logging within AdonisJS. Never trust Firestore document IDs or field values when they originate from external sources. Always validate and sanitize before using them in redirects, headers, or log entries.
1. Validate and sanitize inputs before Firestore operations
Use AdonisJS schema validation to reject or normalize CR and LF characters in user-controlled strings that may become document IDs or Firestore fields. For IDs that must be safe for logging or headers, strip or replace control characters.
import { schema } from '@ioc:Adonis/Core/Validator'
const userSchema = schema.create({
email: schema.string.email(),
// Reject or normalize newlines and carriage returns
username: schema.string.regex(/^[\w\-\.]+$/).minLength(3),
})
export default class UsersController {
public async create({ request, response }){
const payload = await request.validate({ schema: userSchema })
// Ensure no CRLF in fields used in headers or logs
const safeUsername = payload.username.replace(/[\r\n]+/g, '')
const docRef = firestore.collection('users').doc(safeUsername)
await docRef.set({ email: payload.email, createdAt: new Date() })
return response.created({ id: docRef.id })
}
}
2. Avoid using Firestore data directly in Location or custom headers
When constructing redirects or custom headers, do not concatenate Firestore document IDs or fields without strict sanitization. Prefer using an internal mapping (e.g., numeric IDs) for redirects, or encode the value if it must be used in a header context.
import { HttpContextContract } from '@ioc:Adonis/Core/HttpContext'
export default class SessionsController {
public async store({ request, response, auth }: HttpContextContract) {
const { email } = request.only(['email'])
const userDoc = await firestore.collection('users').where('email', '==', email).limit(1).get()
const user = userDoc.docs[0]?.data()
if (!user) return response.unauthorized()
// Unsafe: using Firestore ID directly in Location
// return response.redirect(`/dashboard/${userDoc.id}`)
// Safer: use an internal numeric ID or a sanitized slug
const safeId = userDoc.id.replace(/[\r\n]+/g, '')
return response.redirect(`/dashboard/${safeId}`)
}
}
3. Sanitize Firestore data before logging
When logging Firestore documents, strip or escape CR and LF characters to prevent log injection. Do not directly interpolate raw Firestore snapshots into log messages.
import Logger from '@ioc:Adonis/Core/Logger'
export default class AuditService {
static logDocument(path: string, data: Record) {
const sanitized = Object.entries(data).reduce((acc, [key, value]) => {
const safeValue = typeof value === 'string' ? value.replace(/[\r\n]+/g, ' ') : value
acc[key] = safeValue
return acc
}, {} as Record)
Logger.info(`FirestoreWrite: ${path}`, sanitized)
}
}
// Usage
await Firestore.logDocument('users/123', snapshot.data())
4. Use Firestore settings that minimize risk
Structure Firestore documents to avoid storing raw user input in fields that influence HTTP behavior. If you must store free-form text, enforce length and character restrictions at the application layer. When generating document IDs, prefer server-assigned IDs or a strict naming convention that excludes control characters.
// Prefer server-assigned IDs to avoid injection via client-supplied IDs
const docRef = firestore.collection('posts').doc()
await docRef.set({ title: 'Hello\r\nWorld', content: 'Safe storage' })
console.log('Document ID:', docRef.id) // safe, server-generated
5. Encode output for logging and UI
When displaying Firestore data in views or logs, HTML-encode or otherwise escape newlines and special characters to prevent rendering issues or log forging.
const escapeForLog = (str: string): string => {
return str.replace(/[\r\n]+/g, ' ').replace(/[\t]+/g, ' ')
}
const text = escapeForLog(snapshot.get('description'))
Logger.debug(`Description: ${text}`)