Symlink Attack in Feathersjs with Basic Auth
Symlink Attack in Feathersjs with Basic Auth — how this specific combination creates or exposes the vulnerability
A symlink attack in a Feathersjs application using Basic Auth occurs when an authenticated (or sometimes unauthenticated) user can control file paths or names in a way that causes the application to read or write files outside the intended directory. Because Feathersjs is often used to serve user-uploaded assets or manage file-backed resources, endpoints that accept a filename or path parameter can become a vector if path traversal or symlink resolution is not strictly prevented. Basic Auth in this context is not a direct cause of the symlink issue, but it influences how the attack surface is exposed.
Consider an authenticated API route where a user can specify a target filename to download or a location to store a file. If the application resolves user input directly to the filesystem without canonicalizing the path, a malicious authenticated user can supply a crafted path such as ../../../etc/passwd or a path that traverses into a directory served by the web server. On the server side, if the resolved path lands on a file that is later accessed via a URL exposed by the web server, the user may be able to leverage a symbolic link placed in a writable location (e.g., a temporary or user-controlled directory) that points to a sensitive file. When the application later reads the file through the symlink, it unintentionally exposes the sensitive content.
With Basic Auth, the application typically identifies the user via an Authorization header. The vulnerability is not in the authentication mechanism itself but in how the application uses the identity associated with the authenticated subject. For example, an authenticated user might be allowed to store files under a user-specific directory derived from their identity (e.g., uploads tied to user ID). If the application does not validate or sanitize filenames, the user can create a symlink within their writable directory that points to a file outside that directory. If the application later reads user-controlled paths without ensuring they resolve inside the intended base directory, the symlink enables directory traversal. In a Feathersjs service, this can manifest in a custom hook or a custom transport where file operations occur without strict path confinement.
Real-world examples include services that store user-uploaded images or documents and allow filename specification without strict validation. An authenticated attacker could attempt paths like ../../../../var/www/html/config/database.yml, or they could first place a symlink on the server (e.g., via another vector such as insecure file upload or insecure temporary file handling) that points to a sensitive system file. If the application resolves the path and reads it, the confidentiality of the sensitive file can be compromised. The risk is elevated when the application runs with elevated privileges or when the filesystem permissions are misconfigured.
To detect this category of issue, scans examine whether endpoints that handle file paths or names perform canonicalization and ensure that resolved paths remain within the intended directory. They also check whether user-controlled input reaches filesystem operations without adequate validation. In the context of Basic Auth, findings will highlight whether identity-derived data (such as user IDs) is used to scope file operations without additional path confinement, and whether the application exposes endpoints that can traverse directory boundaries or follow symlinks to sensitive locations.
Basic Auth-Specific Remediation in Feathersjs — concrete code fixes
Remediation focuses on strict input validation, canonical path resolution, and avoiding user-controlled data in filesystem operations. In Feathersjs, you should implement path confinement for any file-related service hooks and sanitize filenames before using them in filesystem calls.
Example: Safe file service with Basic Auth in Feathersjs
Below is a realistic Feathersjs service implementation that uses Basic Auth via an authentication hook and ensures uploaded filenames are confined to an intended directory. It uses Node.js built-in utilities to prevent path traversal and symlink-based escapes.
const path = require('path');
const fs = require('fs/promises');
// Assume an auth hook has already attached user info to params
// e.g., using @feathersjs/authentication-local with a custom hook
const uploadDirectory = path.resolve(__dirname, '..', 'uploads');
function sanitizeFilename(input) {
// Remove path segments and control characters, allow only safe characters
const basename = path.basename(input);
if (!basename || basename.includes('..') || basename.includes('\\')) {
throw new Error('Invalid filename');
}
return basename;
}
// Example service hook
async function ensureSafeFileContext(hook) {
const { user, data } = hook.params;
if (!user || !data || !data.file) {
throw new Error('User or file data missing');
}
const safeName = sanitizeFilename(data.file.name);
const destination = path.join(uploadDirectory, safeName);
// Ensure the resolved path is within uploadDirectory
const resolved = path.resolve(destination);
if (!resolved.startsWith(uploadDirectory)) {
throw new Error('Path traversal detected');
}
// Example: move uploaded file to the safe location
// In practice, you would handle the uploaded buffer/stream appropriately
// fs.remove(destination); // optional cleanup
// await fs.rename(uploadedPath, resolved);
hook.params.filePath = resolved;
return hook;
}
module.exports = function (app) {
app.use('/files', {
async create(data, params) {
// This would normally be invoked by a hook; shown here for illustration
const safeName = sanitizeFilename(data.file.name);
const filePath = path.join(uploadDirectory, safeName);
// Write file safely
// await fs.writeFile(filePath, data.buffer);
return { path: filePath };
}
});
app.hooks({
before: {
all: [],
create: [ensureSafeFileContext]
],
after: {
all: []
},
error: {
all: []
}
});
};
Key practices demonstrated:
- Use
path.basename()to strip directory components from user-supplied filenames. - Resolve the intended base directory with
path.resolve()and verify that the final resolved path starts with that base directory before performing any filesystem operation. - Reject paths containing sequences like
..or backslashes, and avoid concatenating user input directly into paths. - Keep sensitive files outside of web-accessible directories and ensure the web server does not expose the uploads directory as a static asset route that could be leveraged via symlink tricks.
Additionally, review server-level controls: ensure the process does not run with unnecessary privileges, restrict write permissions on directories where symlinks could be created, and monitor for unexpected symlinks in sensitive locations. These measures reduce the impact of any potential symlink-based exposure even if other controls fail.