Time Of Check Time Of Use in Adonisjs with Mutual Tls
Time Of Check Time Of Use in Adonisjs with Mutual Tls — how this specific combination creates or exposes the vulnerability
Time Of Check Time Of Use (TOCTOU) is a class of race condition where a decision is made based on a check that occurs before the actual use of a resource, and the resource state can change between the check and the use. In AdonisJS, this commonly manifests when authorization checks (e.g., ownership or role verification) happen before file or data operations, but the underlying resource can be swapped or altered by an attacker in the intervening time.
Mutual TLS (mTLS) provides strong transport-layer authentication by requiring both client and server to present and validate certificates. While mTLS ensures that the communicating peer is trusted, it does not eliminate application-level authorization or file-system race conditions. In AdonisJS, if you use mTLS to identify a user (e.g., via certificate fields like CN or SAN) and then perform a TOCTOU-prone workflow—such as resolving a file path from certificate-provided input before opening or writing—the window between the check and the use remains exploitable.
A concrete scenario: An endpoint uses mTLS to extract a username from the client certificate and checks that a directory owned by that user exists and is writable. The check verifies permissions and path ownership. Immediately after, the code opens or writes a file using a path derived from user input. An attacker able to manipulate the filesystem between the check and the use (e.g., via a symlink attack if directory traversal or insecure temp file creation is possible) can redirect the operation to a different file, leading to unauthorized read/write or privilege escalation. AdonisJS does not inherently protect against this class of race condition; mTLS only authenticates the client, not the integrity of the runtime state between check and use.
Another pattern is when mTLS client details are used to perform an authorization check (e.g., verifying group membership in a certificate extension), and then the application performs an operation whose outcome depends on dynamic data that can change after the check. For example, reading a configuration record by ID that is validated against certificate claims, then acting on that record. If an attacker can modify the record between validation and use (or if the ID is mutable via path manipulation), the authorization decision becomes stale and unsafe.
To mitigate TOCTOU in the context of mTLS in AdonisJS, minimize the reliance on filesystem checks that precede operations, avoid using mutable external state as the sole basis for decisions, and design flows so that authorization and resource usage are as tightly coupled as possible—ideally a single, atomic operation that encapsulates both verification and action. mTLS should be treated as a strong identity signal, not a substitute for secure coding practices around resource handling.
Mutual Tls-Specific Remediation in Adonisjs — concrete code fixes
Remediation focuses on removing or shrinking the time window between check and use, avoiding unsafe filesystem operations, and ensuring that decisions derived from mTLS attributes are verified atomically with the operation. Below are concrete, realistic patterns for AdonisJS.
- Use atomic file operations instead of check-then-act patterns. Prefer
fs.promises.openwith exclusive creation flags rather than checking existence and permissions first:
import { open, constants } from 'fs/promises';
// Instead of checking permissions then writing, open with exclusive flag
const fd = await open(filePath, 'w', 0o600);
await fd.writeFile(content);
await fd.close();
- When identity comes from mTLS, resolve paths securely without relying on mutable external state. Use a strict allowlist of permitted paths or a mapping verified in a single step:
import { createServer } from 'tls';
import { readFile } from 'fs/promises';
// Example: map certificate subject to an allowed base directory and resolve safely
async function getSecurePath(certSubject: string, userProvided: string): Promise {
const base = '/srv/uploads';
// Validate certSubject against your mTLS trust policy elsewhere; here we focus on path safety
const normalized = userProvided.replace(/^(\.\.[\/\\])+/, '');
const resolved = `${base}/${normalized}`;
if (!resolved.startsWith(base)) {
throw new Error('Path traversal attempt');
}
return resolved;
}
// Usage within an AdonisJS route handler
import { HttpContextContract } from '@ioc:Adonis/Core/HttpContext';
export default async function readProfile({ request, response }: HttpContextContract) {
const subject = request.tls?.subject; // populated by mTLS middleware
const file = await getSecurePath(subject, request.qs().file);
const data = await readFile(file, 'utf-8');
return response.send(data);
}
- For operations that must remain dynamic, use database transactions or file locks to ensure consistency between verification and use. With AdonisJS and a database, wrap the check and the action in a transaction:
import Database from '@ioc:Adonis/Lucid/Database';
export default async function updateConfigSafe() {
await Database.transaction(async (trx) => {
const record = await trx.from('config').where('id', 123).forUpdate().first();
if (!record || !isValid(record)) {
throw new Error('Invalid or missing config');
}
await trx.from('config').where('id', 123).update({ value: 'new' });
});
}
- Apply strict input validation on any data used to derive file paths or database queries, even when mTLS is present. Use schema-driven validation libraries compatible with AdonisJS (e.g., Validator) to enforce format and constraints before any filesystem or database access:
import { schema, rules } from '@ioc:Adonis/Core/Validator';
const fileSchema = schema.create({
file: schema.string({}, [rules.required(), rules.file({ size: '4mb' })]),
});
export default async function upload({ request, response }: HttpContextContract) {
const payload = await request.validate({ schema: fileSchema });
// Safe to proceed with payload.file; validation ensures constraints
}
- Avoid storing sensitive decisions derived from mTLS attributes in mutable external state between requests. Cache identity-to-permission mappings with short TTLs or re-validate on each operation to prevent stale authorizations from being used after state changes.