Improper Limitation of a Pathname to a Restricted Directory ('Path Traversal')
The product uses external input to construct a pathname that is intended to identify a file or directory that is located underneath a restricted parent directory, but the product does not properly neutralize special elements within the pathname that can cause the pathname to resolve to a location that is outside of the restricted directory.
Many file operations are intended to take place within a restricted directory. By using special elements such as '..' and '/' separators, attackers can escape outside of the restricted location to access files or directories that are elsewhere on the system.
How to fix this vulnerability
Prevention strategies for Path Traversal based on 6 Shoulder detection rules.
Resolve the full path and verify it stays within the intended base directory
package main import ( "net/http" "os" - ) - - func handler(w http.ResponseWriter, r *http.Request) { - filename := r.URL.Query().Get("file") - // Vulnerable: user input directly in file path - data, err := os.ReadFile("/uploads/" + filename) + "path/filepath" + "strings" + ) + + const baseDir = "/uploads" + + func handler(w http.ResponseWriter, r *http.Request) { + filename := r.URL.Query().Get("file") + // Clean and resolve the path + cleanName := filepath.Clean(filename) + fullPath := filepath.Join(baseDir, cleanName) + absPath, _ := filepath.Abs(fullPath) + absBase, _ := filepath.Abs(baseDir) + // Verify path is within base directory + if !strings.HasPrefix(absPath, absBase+string(os.PathSeparator)) { + http.Error(w, "Forbidden", 403) + return + } + data, err := os.ReadFile(absPath) if err != nil { http.Error(w, "Not found", 404) return } w.Write(data) }
Validate that extracted archive paths resolve within the target directory
package main import ( "archive/zip" - "io" - "os" - ) - - func extractZip(zipPath, destDir string) error { - r, _ := zip.OpenReader(zipPath) - defer r.Close() - for _, f := range r.File { - // Vulnerable: using archive filename directly - outFile, _ := os.Create(f.Name) + "errors" + "io" + "os" + "path/filepath" + "strings" + ) + + func extractZip(zipPath, destDir string) error { + r, _ := zip.OpenReader(zipPath) + defer r.Close() + destDir = filepath.Clean(destDir) + string(os.PathSeparator) + for _, f := range r.File { + // Safe: validate path is within destination + destPath := filepath.Join(destDir, filepath.Clean(f.Name)) + if !strings.HasPrefix(destPath, destDir) { + return errors.New("illegal file path in archive: " + f.Name) + } + if f.FileInfo().IsDir() { + os.MkdirAll(destPath, 0755) + continue + } + outFile, _ := os.Create(destPath) rc, _ := f.Open() io.Copy(outFile, rc) rc.Close() outFile.Close() } return nil }
Use path.basename() to strip directory components or validate resolved paths stay within allowed directories
const express = require('express'); const fs = require('fs'); const path = require('path'); const app = express(); app.get('/files/:filename', (req, res) => { - const filePath = path.join(__dirname, 'uploads', req.params.filename); + const safeName = path.basename(req.params.filename); + const filePath = path.join(__dirname, 'uploads', safeName); fs.readFile(filePath, (err, data) => { if (err) return res.status(404).send('Not found'); res.send(data); }); });
Validate that extracted archive entry paths resolve within the target directory before writing
const express = require('express'); const AdmZip = require('adm-zip'); const path = require('path'); const app = express(); - app.post('/upload', (req, res) => { - const zip = new AdmZip(req.file.path); - const entries = zip.getEntries(); - for (const entry of entries) { - zip.extractEntryTo(entry, './uploads/', false, true); + function isPathSafe(baseDir, targetPath) { + const resolvedBase = path.resolve(baseDir); + const resolvedTarget = path.resolve(baseDir, targetPath); + return resolvedTarget.startsWith(resolvedBase + path.sep); + } + + app.post('/upload', (req, res) => { + const targetDir = './uploads/'; + const zip = new AdmZip(req.file.path); + const entries = zip.getEntries(); + for (const entry of entries) { + if (!isPathSafe(targetDir, entry.entryName)) { + return res.status(400).json({ error: 'Path traversal detected' }); + } + zip.extractEntryTo(entry, targetDir, false, true); } res.json({ success: true }); });
Use os.path.basename() or pathlib to restrict file access to intended directories
- from flask import request - - @app.route('/read') - def read_file(): - filename = request.args.get('file') - with open(f'/var/uploads/{filename}', 'r') as f: + import os + from flask import request, abort + + UPLOAD_DIR = '/var/uploads' + + @app.route('/read') + def read_file(): + filename = request.args.get('file', '') + safe_name = os.path.basename(filename) + filepath = os.path.join(UPLOAD_DIR, safe_name) + if not os.path.isfile(filepath): + abort(404) + with open(filepath, 'r') as f: return f.read()
Validate extracted file paths stay within the target directory before writing
import zipfile - - def extract_archive(zip_path, dest): - with zipfile.ZipFile(zip_path, 'r') as zf: - zf.extractall(dest) + import os + + def safe_extract(zip_path, dest): + with zipfile.ZipFile(zip_path, 'r') as zf: + for member in zf.namelist(): + target = os.path.normpath(os.path.join(dest, member)) + if not target.startswith(os.path.abspath(dest)): + raise ValueError(f"Path traversal: {member}") + zf.extract(member, dest)
Find vulnerabilities in your code
Use Shoulder to scan your codebase for Improper Limitation of a Pathname to a Restricted Directory ('Path Traversal') patterns. 6 rules.
# Scan with Shoulder CLI npx @shoulderdev/cli trust --cwe=22 # Or scan entire project npx @shoulderdev/cli trust .
Detection Rules (6)
What to watch for in code reviews
These patterns indicate potential Improper Limitation of a Pathname to a Restricted Directory ('Path Traversal') vulnerabilities. Look for these during code reviews and security audits.
Scan your codebase for Improper Limitation of a Pathname to a Restricted Directory ('Path Traversal')
Shoulder CLI finds vulnerable patterns across your entire codebase.