Double Free in Adonisjs (Typescript)
Double Free in Adonisjs with Typescript
In Adonisjs applications written in Typescript, a double free vulnerability can emerge when objects allocated in the heap are deallocated twice without proper nullification, leading to use-after-free conditions that attackers can exploit.
This typically occurs when a controller or service retains a reference to a resource after it has been destroyed, and the runtime attempts to deallocate it again during garbage collection or request teardown.
Adonisjs encourages the use of dependency injection and middleware, which can inadvertently preserve references to session data, database connections, or temporary files across requests if not carefully managed.
For example, consider a controller that creates a temporary file and returns a response while also storing the file handle in a global cache for later cleanup:
import { HttpContextContract } from '@ioc:Adonis/Core/HttpContext';
import TempFileService from 'App/Services/TempFileService';
export default class FileUploadController {
public async upload({ request, response }: HttpContextContract) {
const file = request.file('file', {
types: ['image/jpeg', 'image/png'],
size: '2mb',
});
if (!file) {
return response.badRequest('No file provided');
}
const filename = await TempFileService.create(file);
// Store reference for later cleanup
TempFileService.registerCleanup(filename);
return response.ok({ filename });
}
}
The vulnerability arises when the same filename is registered multiple times or when the cleanup process is triggered more than once, such as during a route reload or when a middleware re-invokes cleanup logic.
Because Adonisjs lifecycle hooks (like onShutdown) may invoke cleanup routines for each request context, failing to nullify the stored reference after deallocation can result in a double free.
In the following example, the service maintains a static map of filenames to cleanup callbacks. If two concurrent requests register the same temporary file, the cleanup function may be bound twice:
export default class TempFileService {
private static cleanupMap: Record void> = {};
public static async create(file: UploadedFile): Promise {
const filename = `temp-${Date.now()}-${Math.random().toString(36).slice(2)}`;
const path = `./tmp/${filename}`;
await file.moveTo('./tmp', filename);
return filename;
}
public static registerCleanup(filename: string) {
if (this.cleanupMap[filename]) {
// Already registered — potential double free
return;
}
this.cleanupMap[filename] = async () => {
const fullPath = path.join('tmp', filename);
try {
await fs.promises.unlink(fullPath);
delete this.cleanupMap[filename];
} catch (e) {
// Log error but do not rethrow
}
};
// Bind to request lifecycle
Contract.boot().on('requestEnd', this.cleanupMap[filename]);
}
}
If two requests call registerCleanup with the same filename before either has completed, the lifecycle hook may fire twice, invoking the cleanup function twice on the same file path.
This creates a double free scenario when the underlying file system handle is closed twice, potentially corrupting memory or allowing an attacker to manipulate file state between deletions.
Such vulnerabilities are exacerbated in Typescript due to its structural typing, where interfaces do not enforce runtime immutability, and shared mutable state across requests can be inadvertently introduced through singletons or static members.
Additionally, Adonisjs middleware chains may process the same request multiple times during retries or error handling, further increasing the risk of repeated cleanup attempts.
Real-world incidents involving similar patterns have been documented in CVE reports related to file deserialization flaws in Node.js ecosystems, where improper cleanup of temporary artifacts led to information disclosure or service disruption.
To mitigate these risks, developers must ensure that lifecycle bindings are unique per request and that references are explicitly cleared after use.