Unicode Normalization in Adonisjs
How Unicode Normalization Manifests in Adonisjs
Unicode normalization attacks in Adonisjs often exploit the framework's handling of user input through its Lucid ORM and validation middleware. When Adonisjs processes HTTP requests, it passes raw query parameters and body data through its validation pipeline before reaching controllers or database operations.
The vulnerability typically appears in Adonisjs route handlers that accept string parameters for database queries or authentication. Consider this common Adonisjs pattern:
Route.get('/users/:id', async ({ params }) => {
const user = await User.find(params.id)
return user
})An attacker can exploit Unicode normalization by sending visually identical but canonically different characters. For example, the username "admin" might have multiple Unicode representations:
// Normal ASCII
admin
// With combining characters
admin
// With compatibility characters
adminIn Adonisjs applications, this becomes dangerous when combined with case-insensitive comparisons or when user input is used in SQL queries without proper normalization. The framework's default behavior doesn't automatically normalize Unicode strings, allowing attackers to bypass authentication checks or access unauthorized resources.
A specific Adonisjs attack pattern involves using Unicode characters that look identical to database entries but have different byte representations. An attacker might register an account with a username containing Unicode combining marks, then use that account to bypass admin-only routes that perform simple string comparisons.
Another manifestation occurs in Adonisjs route parameter binding. When a route expects an integer ID but receives a Unicode string, Adonisjs's type coercion might silently convert visually similar characters, leading to unexpected database queries or even SQL injection in edge cases.
Adonisjs-Specific Detection
Detecting Unicode normalization issues in Adonisjs applications requires examining both the codebase and runtime behavior. Start by auditing your Adonisjs routes and controllers for patterns that accept string input without validation.
middleBrick's API security scanner specifically detects Unicode normalization vulnerabilities in Adonisjs applications by testing endpoints with Unicode variants of common attack strings. The scanner sends requests containing characters from different Unicode normalization forms (NFC, NFD, NFKC, NFKD) and analyzes the responses for inconsistencies.
Here's how middleBrick identifies the issue:
// middleBrick tests with Unicode variants
const testCases = [
'admin', // ASCII
'admin', // NFD form
'admin', // NFKD form
'admin' // Full-width variants
]For Adonisjs applications, middleBrick examines:
- Route handlers that use string parameters without explicit validation
- Database queries that compare user input directly
- Authentication middleware that doesn't normalize credentials
- API responses that reveal whether different Unicode forms are treated identically
You can also manually test your Adonisjs application by creating a simple middleware that logs the byte length and Unicode normalization form of incoming parameters:
export class UnicodeLoggerMiddleware {
async handle({ request }, next) {
const params = request.all()
Object.keys(params).forEach(key => {
const value = params[key]
console.log(`${key}:`, {
value,
length: value.length,
bytes: Buffer.from(value).length,
normalization: require('unorm').nfc(value) === value ? 'NFC' : 'Other'
})
})
await next()
}
}This middleware helps identify parameters that accept Unicode input without proper handling, which is the first step in detecting normalization vulnerabilities.
Adonisjs-Specific Remediation
Remediating Unicode normalization vulnerabilities in Adonisjs requires a multi-layered approach. The most effective solution is to implement consistent Unicode normalization at the application boundary using Adonisjs middleware.
Create a normalization middleware in Adonisjs:
import { normalize } from 'unorm'
export class UnicodeNormalizerMiddleware {
async handle({ request }, next) {
// Normalize all incoming string parameters
const normalizedParams = this.normalizeObject(request.all())
request.body = normalizedParams
// Also normalize query parameters
const normalizedQuery = this.normalizeObject(request.qs())
request.qs = () => normalizedQuery
await next()
}
normalizeObject(obj) {
return Object.keys(obj).reduce((acc, key) => {
const value = obj[key]
if (typeof value === 'string') {
// Use NFC (Canonical Decomposition, followed by Canonical Composition)
acc[key] = normalize(value, 'NFC')
} else if (typeof value === 'object' && value !== null) {
acc[key] = this.normalizeObject(value)
} else {
acc[key] = value
}
return acc
}, {})
}
}Register this middleware globally in your Adonisjs application to ensure all incoming requests are normalized before reaching your business logic.
For database operations, Adonisjs's Lucid ORM provides hooks where you can implement additional validation. Create a base model that enforces strict string validation:
import { DateTime } from 'luxon'
import { BaseModel, column, beforeSave } from '@adonisjs/lucid/src/Orm'
export class SecureModel extends BaseModel {
@beforeSave()
static normalizeStrings(model) {
const columns = model.$columns
Object.keys(columns).forEach(columnName => {
const column = columns[columnName]
if (column.type === 'string' && model[columnName]) {
// Ensure stored strings are in NFC form
model[columnName] = normalize(model[columnName], 'NFC')
}
})
}
@beforeSave()
static validateUnicode(model) {
const columns = model.$columns
Object.keys(columns).forEach(columnName => {
const value = model[columnName]
if (typeof value === 'string') {
// Reject strings with dangerous Unicode patterns
if (/[^ -]/.test(value)) {
throw new Error(`Unicode validation failed for ${columnName}: ${value}`)
}
}
})
}
}Then extend this base model for all your Adonisjs models:
import { column } from '@adonisjs/lucid/src/Orm'
import SecureModel from './SecureModel'
export default class User extends SecureModel {
@column({ isPrimary: true })
public id: number
@column()
public username: string
@column()
public email: string
}For authentication in Adonisjs, implement strict comparison with normalization:
import { compareSync } from 'bcryptjs'
import { normalize } from 'unorm'
export class SecureAuthService {
async authenticate(username, password) {
const normalizedUsername = normalize(username, 'NFC')
const user = await User
.query()
.where('username', normalizedUsername)
.first()
if (!user) {
return null
}
const passwordsMatch = await compareSync(password, user.password)
if (!passwordsMatch) {
return null
}
return user
}
}This approach ensures that authentication in your Adonisjs application is resistant to Unicode-based bypass attempts.