Overview
Path Traversal (also known as Directory Traversal) allows an attacker to read arbitrary files on the server. The vulnerability occurs when an application uses user-supplied input to construct a path to a file or directory without proper validation. By using../ sequences, an attacker can navigate outside of the intended directory to access sensitive files anywhere on the server’s file system.
Business Impact
This vulnerability can lead to the complete disclosure of application source code, configuration files containing credentials, business data, and sensitive operating system files. This information leak is often a precursor to a full system compromise.Reference Details
CWE ID: CWE-22
OWASP Top 10 (2021): A01:2021 - Broken Access Control
Severity: High
Framework-Specific Analysis and Remediation
No framework can automatically protect against this logical vulnerability. The developer is always responsible for sanitizing and validating user input before using it in any file system operation. The core principle is to ensure the final, resolved (canonical) path of the requested file is located within the expected, secure base directory.- Python
- Java
- .NET(C#)
- PHP
- Node.js
- Ruby
Framework Context
Django does not provide a specific file-serving view for arbitrary files, forcing developers to write their own. This is where vulnerabilities are often introduced. The key is to use Python’sos.path module to safely construct and validate paths.Vulnerable Scenario 1: A Document Download View
An endpoint allows users to download invoices by providing a filename.Copy
# invoicing/views.py
import os
from django.http import HttpResponse
def download_invoice(request, filename):
# DANGEROUS: The filename is directly concatenated to the base path.
# An attacker can request a URL like /invoices/download/../../../../etc/passwd
file_path = os.path.join('/var/www/invoices/', filename)
if os.path.exists(file_path):
with open(file_path, 'rb') as fh:
return HttpResponse(fh.read(), content_type="application/pdf")
# ...
Vulnerable Scenario 2: Dynamic Template Loading
A feature loads a custom user theme template based on a cookie value.Copy
# themes/utils.py
def load_user_theme(request):
theme_name = request.COOKIES.get('theme', 'default')
# DANGEROUS: The theme name is used to construct a path to an include file.
template_path = f"themes/{theme_name}.html"
return render_to_string(template_path)
Mitigation and Best Practices
Never trust the filename. Useos.path.basename to strip any directory information. Then, construct the full path and use os.path.abspath to resolve it to its canonical form. Finally, check that this resolved path starts with the secure base directory’s path.Secure Code Example
Copy
# invoicing/views.py (Secure Version)
import os
from django.http import HttpResponse, Http404
def download_invoice(request, filename):
# Define the secure base directory where invoices are stored.
base_dir = os.path.abspath('/var/www/invoices/')
# 1. Strip any directory traversal characters from the filename.
safe_filename = os.path.basename(filename)
# 2. Construct the full path safely.
file_path = os.path.join(base_dir, safe_filename)
# 3. Resolve the path to its absolute form and check if it's within the base directory.
if not os.path.abspath(file_path).startswith(base_dir):
raise Http404("Invalid path detected.")
if os.path.exists(file_path):
with open(file_path, 'rb') as fh:
return HttpResponse(fh.read(), content_type="application/pdf")
else:
raise Http404("File not found.")
Testing Strategy
Write integration tests that request files using path traversal payloads (../, ..%2f, etc.). The tests should assert that the application returns a 404 Not Found or 403 Forbidden response, not the content of the targeted sensitive file.Copy
# invoicing/tests.py
def test_path_traversal_attack_is_blocked(self):
# Attempt to access a file outside the intended directory
traversal_payload = "../../../../../etc/passwd"
url = reverse('download-invoice', args=[traversal_payload])
response = self.client.get(url)
# A secure implementation should detect the invalid path and return an error.
self.assertEqual(response.status_code, 404)
Framework Context
Java’sFile and Path APIs provide the tools needed to prevent path traversal. The vulnerability occurs when developers neglect to canonicalize and validate user-provided path fragments.Vulnerable Scenario 1: Log File Viewer
An admin endpoint allows viewing log files by passing a date-stamped filename.Copy
// controller/AdminController.java
@GetMapping("/logs/{filename}")
public String getLogFile(@PathVariable String filename) throws IOException {
// DANGEROUS: The filename is directly appended to the log directory path.
Path logPath = Paths.get("/var/log/app/" + filename);
return Files.readString(logPath);
}
Vulnerable Scenario 2: Serving User-Uploaded Content
A controller serves images from a user’s personal storage directory.Copy
// controller/FileController.java
@GetMapping("/{userId}/{imageName}")
public byte[] getImage(@PathVariable String userId, @PathVariable String imageName) throws IOException {
String path = UPLOAD_DIR + "/" + userId + "/" + imageName;
// DANGEROUS: A malicious imageName like "../../../system.properties"
// could be used to traverse directories.
return Files.readAllBytes(Paths.get(path));
}
Mitigation and Best Practices
Usejava.nio.file.Path to construct paths. After constructing the path, call .normalize() and .toAbsolutePath(). Then, check that the resulting absolute path .startsWith() the absolute path of the secure base directory.Secure Code Example
Copy
// controller/AdminController.java (Secure Version)
import java.nio.file.Path;
import java.nio.file.Paths;
@GetMapping("/logs/{filename}")
public String getLogFile(@PathVariable String filename) throws IOException {
// Define the secure base directory
Path baseDir = Paths.get("/var/log/app/").toAbsolutePath();
Path requestedPath = baseDir.resolve(filename).normalize();
// CRITICAL CHECK: Ensure the normalized path is still within the base directory.
if (!requestedPath.startsWith(baseDir)) {
throw new SecurityException("Path Traversal attempt detected!");
}
if (Files.exists(requestedPath)) {
return Files.readString(requestedPath);
} else {
throw new FileNotFoundException("Log file not found.");
}
}
Testing Strategy
Write JUnit tests that call the endpoint with various traversal payloads. The tests should assert that aSecurityException or another appropriate error is thrown, preventing the file read operation.Copy
// src/test/java/com/example/AdminControllerTest.java
@Test
void getLogFile_withTraversalPayload_shouldThrowException() {
String payload = "../../../../../etc/hostname";
// Assert that the controller method throws an exception, preventing file access.
assertThrows(SecurityException.class, () -> {
adminController.getLogFile(payload);
});
}
Framework Context
TheSystem.IO namespace provides all the necessary tools for secure path handling. As in other frameworks, the vulnerability comes from developer error in concatenating strings rather than using these tools properly.Vulnerable Scenario 1: A “Get File” Endpoint
An API allows retrieving a file from a public share by name.Copy
// Controllers/FilesController.cs
[HttpGet("{fileName}")]
public IActionResult GetFile(string fileName)
{
// DANGEROUS: `Path.Combine` does not prevent traversal on its own.
// If fileName is "..\\..\\web.config", the path will resolve outside the share.
var filePath = Path.Combine("C:\\PublicShare", fileName);
if (!System.IO.File.Exists(filePath))
{
return NotFound();
}
return PhysicalFile(filePath, "application/octet-stream");
}
Vulnerable Scenario 2: Loading a User’s Report Template
A service loads a report template file based on a user’s department name.Copy
// Services/ReportService.cs
public string LoadTemplate(string department)
{
string templatePath = $"./Templates/{department}/report.txt";
// DANGEROUS: If department is "../../secrets", it could read a secret file.
return File.ReadAllText(templatePath);
}
Mitigation and Best Practices
The key is to combine path construction with validation. First, usePath.GetFileName() to strip any directory information from the user-supplied input. Then, use Path.Combine() to build the full path. Finally, get the full, absolute path with Path.GetFullPath() and verify it is inside the intended base directory.Secure Code Example
Copy
// Controllers/FilesController.cs (Secure Version)
[HttpGet("{fileName}")]
public IActionResult GetFile(string fileName)
{
var baseDir = Path.GetFullPath("C:\\PublicShare");
// 1. Strip out any directory information from the input.
var safeFileName = Path.GetFileName(fileName);
// 2. Combine it with the base directory.
var fullPath = Path.Combine(baseDir, safeFileName);
// 3. Get the absolute path and verify it's still inside the base directory.
if (!Path.GetFullPath(fullPath).StartsWith(baseDir))
{
return Forbid("Attempted path traversal.");
}
if (!System.IO.File.Exists(fullPath))
{
return NotFound();
}
return PhysicalFile(fullPath, "application/octet-stream");
}
Testing Strategy
Write an integration test that sends a request to the endpoint with a traversal payload. Assert that the response is a403 Forbidden or 404 Not Found, not a 200 OK with file contents.Copy
// Tests/FilesControllerTests.cs
[Fact]
public async Task GetFile_WithTraversalPayload_ShouldReturnForbidden()
{
var client = _factory.CreateClient();
var traversalPayload = "..%2F..%2Fappsettings.json"; // URL encoded ../
var response = await client.GetAsync($"/api/files/{traversalPayload}");
Assert.Equal(HttpStatusCode.Forbidden, response.StatusCode);
}
Framework Context
PHP’s file handling functions are powerful and low-level, making path traversal a common vulnerability if developers are not careful. Laravel provides helpers, but the core responsibility remains with the developer to validate paths.Vulnerable Scenario 1: A Photo Gallery Endpoint
A controller serves images from a storage directory based on the URL.Copy
// app/Http/Controllers/PhotoController.php
class PhotoController extends Controller
{
public function show($year, $photoName)
{
// DANGEROUS: The photoName is not sanitized.
// A request to /photos/2025/../../.env could expose credentials.
$path = storage_path("app/photos/{$year}/{$photoName}");
if (!file_exists($path)) {
abort(404);
}
return response()->file($path);
}
}
Vulnerable Scenario 2: Using include for Localization
A feature includes a language file based on a user’s cookie.Copy
// app/Http/Middleware/LocalizationMiddleware.php
public function handle(Request $request, Closure $next)
{
$locale = $request->cookie('locale', 'en');
// DANGEROUS: If an attacker can set the cookie to "../.env",
// this could lead to a file inclusion vulnerability.
include resource_path("lang/{$locale}.php");
return $next($request);
}
Mitigation and Best Practices
Use thebasename() function on any user-supplied filename to strip out all directory information. After constructing the path, use realpath() to get the canonical path and check that it’s within the intended base directory. For includes, use a hardcoded allow-list of valid values.Secure Code Example
Copy
// app/Http/Controllers/PhotoController.php (Secure Version)
class PhotoController extends Controller
{
public function show($year, $photoName)
{
$baseDir = realpath(storage_path("app/photos/{$year}"));
if (!$baseDir) { abort(404); }
// 1. Strip any directory traversal from the user input.
$safePhotoName = basename($photoName);
// 2. Construct the path.
$fullPath = $baseDir . DIRECTORY_SEPARATOR . $safePhotoName;
// 3. Verify the final real path is within the allowed directory.
if (realpath($fullPath) && strpos(realpath($fullPath), $baseDir) === 0) {
return response()->file($fullPath);
}
abort(404);
}
}
Testing Strategy
Write a feature test that makes a GET request to the endpoint with a traversal payload. Assert that the response status is404 Not Found or 403 Forbidden.Copy
// tests/Feature/PhotoSecurityTest.php
public function test_photo_endpoint_prevents_path_traversal()
{
$user = User::factory()->create();
$traversalPayload = '../../../../.env';
$response = $this->actingAs($user)
->get('/photos/2025/' . $traversalPayload);
$response->assertStatus(404);
}
Framework Context
The built-inpath module is essential for securely handling file paths in Node.js. Vulnerabilities are common when developers perform simple string concatenation instead of using path.join, path.resolve, and path.normalize.Vulnerable Scenario 1: A Custom Static File Server
A simple server is written to serve files from a ‘public’ directory.Copy
// server.js
const express = require('express');
const fs = require('fs');
const path = require('path');
const app = express();
app.get('/static/*', (req, res) => {
// DANGEROUS: req.params[0] contains the path after /static/.
// If the URL is /static/../../package.json, this will read the project's config file.
const filePath = path.join(__dirname, 'public', req.params[0]);
fs.readFile(filePath, (err, data) => {
if (err) return res.status(404).send('Not Found');
res.send(data);
});
});
Vulnerable Scenario 2: An API to Fetch User Avatars
The API fetches an avatar based on a user ID and filename.Copy
// routes/avatars.js
router.get('/:userId/:fileName', (req, res) => {
const { userId, fileName } = req.params;
const filePath = `./uploads/avatars/${userId}/${fileName}`;
// DANGEROUS: Both userId and fileName can contain `../`
res.sendFile(path.resolve(filePath));
});
Mitigation and Best Practices
Always resolve paths to their absolute form and validate that they are within a secure base directory. Thepath.normalize and path.resolve functions are key. Never trust that path.join on its own is safe.Secure Code Example
Copy
// server.js (Secure Version)
app.get('/static/*', (req, res) => {
const publicDir = path.resolve(__dirname, 'public');
const requestedFile = req.params[0];
// Construct the full path and then resolve it to its canonical form
const fullPath = path.join(publicDir, requestedFile);
const resolvedPath = path.normalize(fullPath);
// CRITICAL CHECK: Ensure the resolved path is still inside the public directory.
if (!resolvedPath.startsWith(publicDir)) {
return res.status(403).send('Forbidden: Access Denied');
}
fs.readFile(resolvedPath, (err, data) => {
if (err) return res.status(404).send('Not Found');
res.send(data);
});
});
Testing Strategy
Use Jest and Supertest to make a request to the server with a traversal payload. Assert that the server responds with a403 Forbidden status code.Copy
// tests/static.test.js
it('should block path traversal attempts', async () => {
const response = await request(app).get('/static/..%2f..%2fpackage.json'); // URL encoded ../../
expect(response.statusCode).toBe(403);
expect(response.text).toContain('Forbidden');
});
Framework Context
Rails abstracts away most direct file system access, but vulnerabilities can still occur in controllers that send files (send_file) or in services that read files from disk based on user input.Vulnerable Scenario 1: A Log Viewer
An admin controller that allows viewing different application log files by name.Copy
# app/controllers/admin/logs_controller.rb
class Admin::LogsController < ApplicationController
def show
log_name = params[:id]
# DANGEROUS: The log name is directly joined to the logs directory path.
# An attacker can set id to "../../../config/database.yml"
log_path = Rails.root.join('log', "#{log_name}.log")
render plain: File.read(log_path)
end
end
Vulnerable Scenario 2: Sending User-Generated Reports
A feature that allows a user to download a report that was previously generated for them.Copy
# app/controllers/reports_controller.rb
def download
report_id = params[:id]
# DANGEROUS: The report_id could be crafted to traverse directories.
file_path = "#{Rails.root}/private/reports/#{current_user.id}/#{report_id}.pdf"
send_file file_path
end
Mitigation and Best Practices
Sanitize the user-provided filename part usingFile.basename. Construct the full path and then use File.expand_path to get its canonical form. Check that this expanded path is within the secure base directory.Secure Code Example
Copy
# app/controllers/admin/logs_controller.rb (Secure Version)
class Admin::LogsController < ApplicationController
def show
base_dir = Rails.root.join('log').to_s
# 1. Strip any directory components from the user input
safe_log_name = File.basename(params[:id])
# 2. Construct the path
log_path = File.join(base_dir, "#{safe_log_name}.log")
# 3. Canonicalize and verify the path is within the base directory
expanded_path = File.expand_path(log_path)
if !expanded_path.start_with?(base_dir)
render plain: "Error: Invalid log file.", status: :forbidden
return
end
render plain: File.read(expanded_path)
end
end
Testing Strategy
Write an RSpec request spec that attempts to access a sensitive file likeconfig/database.yml using a traversal payload. The test should assert that the response is a 403 Forbidden or 404 Not Found.Copy
# spec/requests/admin_logs_spec.rb
it "prevents path traversal when accessing logs" do
# URL encode the payload
traversal_payload = CGI.escape("../config/database.yml").gsub('.', '%2e')
get admin_log_path(id: traversal_payload)
expect(response).to have_http_status(:forbidden)
end

