Path Traversal in Loopback
How Path Traversal Manifests in Loopback
Path traversal vulnerabilities in Loopback applications typically emerge through improper handling of file system operations combined with user-controlled input. In Loopback's architecture, these vulnerabilities often appear in custom remote methods, middleware, or when using Loopback's file upload capabilities.
The most common pattern involves using fs module operations where the file path is constructed from user input without proper validation. For example, a Loopback controller might expose a method to download user files:
import {get} from '@loopback/rest';
import fs from 'fs';
import path from 'path';
export class FileController {
@get('/download', {
responses: {
'200': {
description: 'Download file',
content: {
'application/octet-stream': {
'x-ts-type': Buffer,
},
},
},
},
})
async downloadFile(@param.query.string('filename') filename: string) {
const filePath = path.join(__dirname, '../uploads/', filename);
return fs.promises.readFile(filePath);
}
}This code is vulnerable because an attacker can supply filename=../../etc/passwd to traverse outside the intended directory. The path.join() function normalizes the path, allowing ../ sequences to escape the uploads directory.
Loopback's file upload system can also introduce path traversal risks. When using @loopback/rest's file upload handling, developers might inadvertently expose file paths:
import {post, Request} from '@loopback/rest';
import {UploadFile} from '@loopback/rest/dist/lib/upload-file.decorator';
export class UploadController {
@post('/upload', {
responses: {
'200': {
description: 'File uploaded successfully',
},
},
})
async uploadFile(
@requestBody.file() request: Request,
@UploadFile() upload: UploadFile,
) {
const filePath = upload.file.path; // User-controlled path
// Later, this path might be used without validation
return {message: 'Uploaded', path: filePath};
}
}Another Loopback-specific scenario involves dynamic model resolution. Loopback's model discovery can be exploited if model paths are constructed from user input:
import {get} from '@loopback/rest';
export class ModelController {
@get('/model/{modelName}')
async getModel(@param.path.string('modelName') modelName: string) {
const modelPath = `./models/${modelName}.json`;
return require(modelPath); // Path traversal possible
}
}Loopback's middleware system can also be a vector. Custom middleware that processes file paths without validation can introduce traversal vulnerabilities:
export class StaticFileMiddleware implements MiddlewareFn {
async handle(context: MiddlewareContext) {
const {request} = context;
const filePath = path.join(__dirname, 'public', request.path);
if (fs.existsSync(filePath)) {
return fs.createReadStream(filePath);
}
}
}Loopback-Specific Detection
Detecting path traversal in Loopback applications requires both static code analysis and runtime scanning. Static analysis should focus on identifying patterns where file paths are constructed from user input.
Using middleBrick's scanner, you can identify path traversal vulnerabilities in your Loopback APIs without any configuration. The scanner examines your API endpoints for common traversal patterns:
npm install -g middlebrick
middlebrick scan https://your-loopback-app.com/api
The scanner tests for traversal attempts using various payloads like ../../etc/passwd, ../ sequences, and URL-encoded variants. It also checks for improper use of path.join(), path.resolve(), and other path manipulation functions.
For Loopback applications, middleBrick specifically looks for:
- Remote methods that accept file paths as parameters
- Custom middleware that processes file system operations
- Dynamic model loading based on user input
- File upload handlers that expose file paths
- Static file serving implementations
The scanner provides detailed findings with severity levels and remediation guidance. For example, it might report:
Path Traversal Vulnerability in FileController.downloadFile
Severity: High
Endpoint: GET /download
Issue: User-controlled filename parameter allows directory traversal
Recommendation: Validate filename against whitelist or use path normalizationmiddleBrick's continuous monitoring (Pro plan) can automatically scan your Loopback APIs on a schedule, alerting you to new vulnerabilities as they're introduced during development.
Loopback-Specific Remediation
Remediating path traversal in Loopback applications involves validating and sanitizing file paths before use. The most effective approach combines input validation with safe path handling.
For file download endpoints, implement strict validation:
import {get} from '@loopback/rest';
import fs from 'fs';
import path from 'path';
export class SecureFileController {
@get('/download', {
responses: {
'200': {
description: 'Download file',
content: {
'application/octet-stream': {
'x-ts-type': Buffer,
},
},
},
},
})
async downloadFile(@param.query.string('filename') filename: string) {
// Validate filename - only allow alphanumeric, hyphen, underscore, dot
if (!/^[a-zA-Z0-9._-]+$/.test(filename)) {
throw new HttpErrors.BadRequest('Invalid filename');
}
const uploadDir = path.join(__dirname, '../uploads/');
const filePath = path.resolve(uploadDir, filename);
// Ensure file is within the uploads directory
if (!filePath.startsWith(uploadDir)) {
throw new HttpErrors.Forbidden('Path traversal attempt detected');
}
if (!fs.existsSync(filePath) || !fs.statSync(filePath).isFile()) {
throw new HttpErrors.NotFound('File not found');
}
return fs.promises.readFile(filePath);
}
}For file uploads, use Loopback's built-in file handling with additional validation:
import {post} from '@loopback/rest';
import {UploadFile} from '@loopback/rest/dist/lib/upload-file.decorator';
export class SecureUploadController {
@post('/upload', {
responses: {
'200': {
description: 'File uploaded successfully',
},
},
})
async uploadFile(
@requestBody.file() request: Request,
@UploadFile() upload: UploadFile,
) {
const uploadDir = path.join(__dirname, '../uploads/');
const fileName = upload.file.originalName;
// Validate file name
if (!/^[a-zA-Z0-9._-]+$/.test(fileName)) {
throw new HttpErrors.BadRequest('Invalid filename');
}
const targetPath = path.join(uploadDir, fileName);
// Ensure target path is within upload directory
if (!targetPath.startsWith(uploadDir)) {
throw new HttpErrors.Forbidden('Invalid file path');
}
// Save file securely
await fs.promises.mkdir(uploadDir, {recursive: true});
await fs.promises.rename(upload.file.path, targetPath);
return {message: 'Uploaded successfully', path: targetPath};
}
}For dynamic model loading, use a whitelist approach:
import {get} from '@loopback/rest';
const VALID_MODELS = ['user', 'product', 'order'];
export class ModelController {
@get('/model/{modelName}')
async getModel(@param.path.string('modelName') modelName: string) {
if (!VALID_MODELS.includes(modelName)) {
throw new HttpErrors.NotFound('Model not found');
}
const modelPath = path.join(__dirname, 'models', `${modelName}.json`);
return require(modelPath);
}
}For static file serving, use Loopback's built-in static middleware with a restricted root:
import {Application} from '@loopback/core';
import {RestApplication} from '@loopback/rest';
import path from 'path';
export class SecureApp extends RestApplication {
constructor() {
super();
// Serve static files from a restricted directory
this.static('/', path.join(__dirname, 'public'));
}
}Related CWEs: inputValidation
| CWE ID | Name | Severity |
|---|---|---|
| CWE-20 | Improper Input Validation | HIGH |
| CWE-22 | Path Traversal | HIGH |
| CWE-74 | Injection | CRITICAL |
| CWE-77 | Command Injection | CRITICAL |
| CWE-78 | OS Command Injection | CRITICAL |
| CWE-79 | Cross-site Scripting (XSS) | HIGH |
| CWE-89 | SQL Injection | CRITICAL |
| CWE-90 | LDAP Injection | HIGH |
| CWE-91 | XML Injection | HIGH |
| CWE-94 | Code Injection | CRITICAL |