Double Free in Adonisjs
How Double Free Manifests in Adonisjs
Double Free vulnerabilities in Adonisjs typically emerge through improper memory management in database transactions and file operations. When an application frees the same memory location twice, it creates an exploitable condition where attackers can manipulate memory contents or trigger crashes.
In Adonisjs, this often occurs in transaction rollback scenarios. Consider a database transaction where an error triggers a rollback, but the rollback handler itself contains a bug that attempts to free resources already released during the initial error handling:
const Database = use('Database');
async createTransaction(data) {
const trx = await Database.beginTransaction();
try {
await trx.insert('users').values(data);
await trx.commit();
} catch (error) {
await trx.rollback(); // First free attempt
throw error;
} finally {
// Bug: attempting to free transaction again
await trx.rollback(); // Second free attempt - DOUBLE FREE
}
}Another common pattern appears in file upload handling. Adonisjs's multipart parser can create scenarios where file streams are closed multiple times:
const fs = require('fs');
async uploadFile({ request }) {
const profilePic = request.file('profile_pic', {
types: ['image'],
size: '2mb'
});
await profilePic.move('uploads');
if (!profilePic.moved()) {
// First cleanup
await profilePic.delete();
throw new Error('File upload failed');
// Bug: attempting to delete again in different error path
await profilePic.delete(); // DOUBLE FREE
return profilePic;
}Middleware can also introduce Double Free conditions when request/response objects are manipulated incorrectly. Adonisjs middleware chains can create situations where response streams are ended multiple times:
class ResponseMiddleware {
async handle({ response }, next) {
try {
await next();
} catch (error) {
response.status(500).send('Error');
response.end(); // First end
throw error;
} finally {
response.end(); // Second end - DOUBLE FREE
}
}
}Adonisjs-Specific Detection
Detecting Double Free vulnerabilities in Adonisjs requires both static analysis and runtime monitoring. The framework's IoC container and service providers create specific patterns that security tools must understand.
Static analysis should focus on transaction handling patterns. Look for try-catch-finally blocks where rollback/commit operations appear in multiple places:
# Pattern to search for in Adonisjs codebase
grep -r "rollback();.*rollback();" app/Services/ --include="*.js" --include="*.ts"middleBrick's API security scanner includes specialized detection for Double Free patterns in Node.js applications, including Adonisjs-specific code structures. The scanner analyzes:
- Database transaction lifecycle management
- File stream cleanup operations
- Middleware response handling
- Memory allocation/deallocation patterns in service providers
For runtime detection, implement monitoring that tracks resource lifecycle:
const resourceTracker = new Map();
function trackResource(resource, type) {
if (resourceTracker.has(resource)) {
console.warn(`Potential double free: ${type} already tracked`);
} else {
resourceTracker.set(resource, type);
}
}
function releaseResource(resource) {
if (!resourceTracker.has(resource)) {
console.warn(`Freeing untracked resource: ${resource}`);
} else {
resourceTracker.delete(resource);
}
}middleBrick's continuous monitoring feature can be configured to scan Adonisjs applications on a schedule, catching Double Free vulnerabilities before they reach production. The scanner's black-box approach tests the API surface without requiring source code access.
Adonisjs-Specific Remediation
Remediating Double Free vulnerabilities in Adonisjs requires understanding the framework's lifecycle management and adopting defensive programming patterns. The key is ensuring resources are freed exactly once, regardless of execution path.
For database transactions, use a single cleanup function with proper state tracking:
async createTransaction(data) {
const trx = await Database.beginTransaction();
let cleanupDone = false;
const cleanup = async () => {
if (!cleanupDone) {
await trx.rollback();
cleanupDone = true;
}
};
try {
await trx.insert('users').values(data);
await trx.commit();
cleanupDone = true; // Mark as cleaned up
} catch (error) {
await cleanup();
throw error;
} finally {
// Safe: cleanup function checks state
await cleanup();
}
}File operations should use Adonisjs's built-in stream management rather than manual cleanup:
const Helpers = use('Helpers');
async uploadFile({ request }) {
const profilePic = request.file('profile_pic', {
types: ['image'],
size: '2mb'
});
try {
await profilePic.move(Helpers.publicPath('uploads'));
return profilePic;
} catch (error) {
// Adonisjs handles stream cleanup automatically
throw error;
} finally {
// No manual delete needed - framework manages lifecycle
}
}Middleware should use Adonisjs's response management utilities:
class ResponseMiddleware {
async handle({ response }, next) {
try {
await next();
} catch (error) {
await response.status(500).send('Error');
// No manual end() - Adonisjs handles response lifecycle
throw error;
}
}
}Service providers should use Adonisjs's lifecycle hooks for resource management:
class MyServiceProvider {
async boot() {
this.app.singleton('MyResource', () => {
const resource = createResource();
// Register cleanup with Adonisjs's shutdown hook
this.app.on('close', () => {
cleanupResource(resource);
});
return resource;
});
}
}