Unicode Normalization in Adonisjs with Basic Auth
Unicode Normalization in Adonisjs with Basic Auth — how this specific combination creates or exposes the vulnerability
When an AdonisJS application uses HTTP Basic Authentication, the framework decodes the Authorization header and compares the received credentials against configured or database-backed user identities. If user identifiers (such as usernames or emails) accept Unicode input, differing Unicode normalization forms can lead to authentication bypass or privilege confusion. For example, a normalized username stored in the database (NFC) may not match a client-provided username that is canonically composed (NFD or NFKC), allowing an attacker to authenticate as a different logical account while presenting the same visual characters.
AdonisJS does not automatically normalize Unicode strings before comparison. In a Basic Auth flow, the decoded username:password pair is typically validated in an authentication provider or hook. If the provider normalizes only the stored value and not the runtime input—or vice versa—an attacker can use combining characters or alternate code point sequences to bypass expected matches. This becomes a security-relevant issue when authorization decisions (such as role or scope checks) depend on a username match that appears identical but is not canonically equal.
Because middleBrick tests unauthenticated attack surfaces, it can detect endpoints that accept user-controlled identifiers without consistent normalization and can surface these as authentication-related findings. For example, inconsistent normalization can be correlated with BOLA/IDOR-like logical access issues, where the effective actor differs from the expected identity. Remediation focuses on canonicalizing both stored and runtime values to the same Unicode form and ensuring that comparisons are performed on normalized data before any authorization logic.
Basic Auth-Specific Remediation in Adonisjs — concrete code fixes
To mitigate Unicode normalization issues in AdonisJS with Basic Authentication, normalize usernames (or other identity fields) to a single canonical form before storage and before comparison. Use a Unicode normalization library such as unorm or the built-in Intl API available in Node.js.
Example: Basic Auth provider with normalization
import { HttpContextContract } from '@ioc:Adonis/Core/HttpContext'
import User from 'App/Models/User'
import unorm from 'unorm' // or use: new Intl.Collator(undefined, { usage: 'search', sensitivity: 'base }).compare
export default class AuthController {
public async login({ request, auth, response }: HttpContextContract) {
const header = request.header('authorization') || ''
// Basic Auth: header = 'Basic base64(username:password)'
const match = header.match(/^Basic\s+(\S+)$/i)
if (!match) {
return response.unauthorized()
}
const decoded = Buffer.from(match[1], 'base64').toString('utf8')
const [username, password] = decoded.split(':')
if (!username || !password) {
return response.unauthorized()
}
// Normalize to NFC to ensure consistent comparison
const normalizedUsername = unorm.nfkc(username)
const user = await User.query().where('username', normalizedUsername).first()
if (!user) {
return response.unauthorized()
}
const passwordMatches = await user.verifyPassword(password)
if (!passwordMatches) {
return response.unauthorized()
}
await auth.use('api').login(user)
return response.ok({ token: await user.generateJwt() })
}
}
Key points in this example:
- The Authorization header is parsed without credentials, preserving the unauthenticated scan nature.
- Both the runtime username and the stored username should be normalized to the same form (here, NFKC) before comparison.
- Password verification remains independent of normalization; only the identity field is canonicalized.
Alternative: Centralized auth provider
If you use AdonisJS authentication providers, normalize in the provider rather than the route handler:
import { BaseUserProvider } from '@ioc:Adonis/Extras/Auth'
import User from 'App/Models/User'
import unorm from 'unorm'
class NormalizedUserProvider extends BaseUserProvider {
public async verify(credentials: Record, password: string) {
const normalized = unorm.nfkc(credentials.username)
const user = await User.query().where('username', normalized).first()
if (!user) {
return null
}
const passwordMatches = await user.verifyPassword(password)
return passwordMatches ? user : null
}
}
// In provider configuration (e.g., start/auth.ts):
// import provider = new NormalizedUserProvider()
By normalizing at the provider level, all authentication routes that rely on this provider inherit consistent handling, reducing the risk of missed comparisons across endpoints.
middleBrick can surface inconsistencies in how endpoints handle identity inputs, including cases where normalization is absent. Its checks for Authentication and BOLA/IDOR-style logical access complement these code-level fixes by highlighting endpoints where identity handling may be inconsistent.