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.
如何修复此漏洞
基于 6 条 Shoulder 检测规则的 Path Traversal 预防策略。
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)
查找代码中的漏洞
使用Shoulder扫描代码中的Improper Limitation of a Pathname to a Restricted Directory ('Path Traversal')模式。 6 规则.
# Scan with Shoulder CLI npx @shoulderdev/cli trust --cwe=22 # Or scan entire project npx @shoulderdev/cli trust .
检测规则 (6)
代码审查中需要关注的内容
这些模式表明潜在的Improper Limitation of a Pathname to a Restricted Directory ('Path Traversal')漏洞。在代码审查和安全审计中注意查找。
扫描你的代码库: Improper Limitation of a Pathname to a Restricted Directory ('Path Traversal')
Shoulder CLI 在整个代码库中找到易受攻击的模式。