Container Escape in Loopback
How Container Escape Manifests in Loopback
Container escape vulnerabilities in Loopback applications typically arise when the framework's powerful features are misused to access system resources or when insecure dependencies create unintended escalation paths. In Loopback, this often manifests through improper handling of file system operations, database connections, or external process execution.
One common pattern involves Loopback's file upload capabilities. When applications allow users to upload files without proper validation, attackers can upload malicious scripts or archive files containing path traversal sequences. The following vulnerable code demonstrates this issue:
import {StorageBindings} from '@loopback/storage';
import {inject} from '@loopback/core';
export class FileController {
constructor(
@inject(StorageBindings.CONFIG) private storageConfig: any,
) {}
async upload(@requestBody.file() file: Express.Multer.File) {
// VULNERABLE: No validation of file path or content
const filePath = path.join(this.storageConfig.root, file.originalname);
await fs.promises.writeFile(filePath, file.buffer);
return {message: 'File uploaded successfully'};
}
}An attacker could upload a file named ../../../../etc/passwd to read sensitive system files or upload a reverse shell script that executes when processed by the application.
Another Loopback-specific container escape vector involves the framework's database connector configuration. Loopback's flexible connector system can inadvertently expose database credentials or allow connection strings that point to internal services:
import {inject} from '@loopback/core';
import { juggler } from '@loopback/repository';
export class DatabaseController {
constructor(
@inject('datasources.config.db') private dbConfig: any,
) {}
async testConnection() {
// VULNERABLE: Exposes raw database configuration
const ds = new juggler.DataSource(this.dbConfig);
const connection = await ds.connect();
return {connection: JSON.stringify(this.dbConfig)};
}
}If the database configuration includes internal network addresses or credentials, an attacker who gains access to this endpoint could map the internal network or attempt connections to other services.
Loopback's support for custom scripts and shell commands through the @loopback/boot module can also create container escape opportunities when improperly configured. The following pattern is particularly dangerous:
import {Command} from '@loopback/cli';
export class ShellCommand implements Command {
name = 'exec';
description = 'Execute shell command';
async run(args: string[]) {
// VULNERABLE: Direct shell command execution
const command = args.join(' ');
return execSync(command, {encoding: 'utf8'});
}
}This allows any authenticated user to execute arbitrary commands on the host system, completely bypassing container isolation.
Loopback-Specific Detection
Detecting container escape vulnerabilities in Loopback applications requires a combination of static analysis and runtime scanning. middleBrick's API security scanner includes specific checks for Loopback applications that can identify these issues without requiring source code access.
For file upload vulnerabilities, middleBrick tests for path traversal by attempting to upload files with directory traversal sequences and checking if they're stored outside the intended directory. The scanner also examines file processing endpoints to ensure they don't execute uploaded content. When scanning a Loopback application, middleBrick will specifically look for:
- File upload endpoints that don't validate file paths or content types
- Database configuration endpoints that expose connection strings
- Command execution endpoints that allow arbitrary shell commands
- Storage configuration that allows access to system directories
middleBrick's LLM security features are particularly relevant for Loopback applications that use AI capabilities. The scanner tests for prompt injection vulnerabilities that could lead to data exfiltration or unauthorized code execution:
# Example middleBrick CLI scan for a Loopback API
middlebrick scan https://api.example.com --output json --verbose
# Output snippet showing LLM security findings
{
"llm_security": {
"system_prompt_leakage": "PASS",
"prompt_injection": "FAIL - Active injection test 3 succeeded",
"excessive_agency": "PASS",
"unauthenticated_endpoint": "PASS"
}
}For runtime detection, middleBrick's continuous monitoring (Pro plan) can track changes in your Loopback application's attack surface over time. The scanner will alert you if new endpoints are added that might introduce container escape vulnerabilities or if existing endpoints become more permissive.
Additionally, middleBrick analyzes OpenAPI specifications for Loopback applications to identify potential security issues in the API contract itself. This includes checking for overly permissive file upload configurations, database connection exposure, and command execution endpoints that might not be obvious from the code alone.
Loopback-Specific Remediation
Securing Loopback applications against container escape vulnerabilities requires implementing defense-in-depth strategies and leveraging Loopback's built-in security features. Here are specific remediation techniques for the vulnerabilities discussed:
For file upload security, implement strict validation and use Loopback's storage abstraction properly:
import {inject} from '@loopback/core';
import {StorageBindings} from '@loopback/storage';
import {StorageService} from '@loopback/storage';
export class SecureFileController {
constructor(
@inject(StorageBindings.CONFIG) private storageConfig: any,
@inject(StorageBindings.SERVICE) private storageService: StorageService,
) {}
async upload(@requestBody.file()) {
const file = req.file;
// Validate file type and size
const allowedTypes = ['image/jpeg', 'image/png', 'application/pdf'];
if (!allowedTypes.includes(file.mimetype)) {
throw new HttpErrors.BadRequest('Invalid file type');
}
if (file.size > 5 * 1024 * 1024) { // 5MB limit
throw new HttpErrors.BadRequest('File too large');
}
// Sanitize filename and store securely
const sanitizedName = path.basename(file.originalname);
const filePath = path.join('uploads', sanitizedName);
// Use storage service with proper configuration
await this.storageService.write(filePath, file.buffer);
return {message: 'File uploaded successfully', filePath};
}
}For database connection security, use environment variables and Loopback's configuration validation:
// config/datasources.json
{
"db": {
"name": "db",
"connector": "postgresql",
"host": "${DB_HOST}",
"port": "${DB_PORT}",
"user": "${DB_USER}",
"password": "${DB_PASSWORD}",
"database": "${DB_NAME}"
}
}
// Add validation in your application
import {validate} from 'uuid';
export class DatabaseConfigValidator {
validateConfig(config: any) {
// Ensure no internal network addresses
const internalNetworks = [
/^127\./,
/^10\./,
/^172\.(1[6-9]|2[0-9]|3[0-1])\./,
/^192\.168\./
];
if (internalNetworks.some(regex => regex.test(config.host))) {
throw new Error('Database host cannot be internal network');
}
// Validate other configuration parameters
if (!validate(config.database)) {
throw new Error('Invalid database identifier');
}
}
}For command execution vulnerabilities, eliminate direct shell access and use Loopback's service injection for any necessary system operations:
import {inject} from '@loopback/core';
import {HttpErrors} from '@loopback/rest';
export class SafeCommandController {
constructor(
@inject('services.system') private systemService: SystemService,
) {}
async executeCommand(@param.query.string('command') command: string) {
// VULNERABLE: Never allow arbitrary command execution
throw new HttpErrors.MethodNotAllowed('Command execution not permitted');
}
async safeOperation(@param.query.string('operation') operation: string) {
// SAFE: Use controlled service methods
switch (operation) {
case 'listFiles':
return this.systemService.listFiles('/safe/directory');
case 'getFileStats':
return this.systemService.getFileStats('/safe/directory/file.txt');
default:
throw new HttpErrors.BadRequest('Unknown operation');
}
}
}Finally, implement proper error handling and logging to detect potential container escape attempts:
import {HttpErrors} from '@loopback/rest';
import {get} from '@loopback/openapi-v3';
export class SecurityController {
@get('/health', {
responses: {
'200': {
description: 'Health check',
content: {
'application/json': {
schema: {
type: 'object',
properties: {
status: {type: 'string'},
timestamp: {type: 'string', format: 'date-time'},
},
},
},
},
},
},
})
async healthCheck() {
try {
// Perform security-sensitive operations safely
const secureResult = await this.performSecureCheck();
return {
status: 'healthy',
timestamp: new Date().toISOString(),
security: secureResult,
};
} catch (error) {
// Log security events without exposing details
console.warn('Security check failed:', error.message);
throw new HttpErrors.InternalServerError('Health check unavailable');
}
}
}