Spaces:
Sleeping
Sleeping
File size: 6,465 Bytes
7257069 ef93755 7257069 ef93755 7257069 ef93755 7257069 ef93755 7257069 ef93755 7257069 ef93755 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 | """
SecureCodeEnv Task: File Upload Handler
Difficulty: Hard
CWE Targets: CWE-22 (Path Traversal), CWE-434 (Unrestricted File Upload)
"""
TASK = {
"id": "hard_file_upload_handler",
"difficulty": "hard",
"cwe_targets": ["CWE-22", "CWE-434"],
"component_name": "FileUploadHandler",
"filename": "src/files/file_upload_handler.py",
"problem_statement": """Write a Python function:
`validate_upload(filename: str, content_type: str, file_size: int, upload_dir: str) -> dict`
Validates and prepares a file upload for safe storage.
REQUIREMENTS — ALL must pass:
1. FILENAME SAFETY:
- Strip all path components (only keep the basename)
- Reject filenames with path traversal (../../)
- Reject filenames with null bytes (%00, \\x00)
- Max filename length: 100 characters
- Only allow: alphanumeric, dots, dashes, underscores
2. EXTENSION VALIDATION:
- Allowed extensions: .jpg, .jpeg, .png, .gif, .pdf, .txt, .csv
- Block ALL executable extensions: .php, .py, .sh, .exe, .js, .html, .htm, .asp, .jsp
- Extension check must be case-insensitive (.PHP is same as .php)
3. MIME TYPE VALIDATION:
- Must cross-check content_type against allowed types:
image/jpeg, image/png, image/gif, application/pdf, text/plain, text/csv
- Reject mismatches between extension and content_type (e.g., .jpg with text/html)
4. FILE SIZE LIMITS:
- Max size: 10MB (10 * 1024 * 1024 bytes)
- Reject files over the limit
5. SAFE PATH GENERATION:
- Returns: {"ok": True, "safe_path": "<upload_dir>/<uuid>_<safe_filename>", "safe_filename": str}
- Uses uuid4 prefix to prevent filename collisions
- On rejection: {"ok": False, "reason": "<specific reason>"}
NEVER trust content_type alone — always validate extension too.""",
"starter_code": '''import os
def validate_upload(filename: str, content_type: str, file_size: int, upload_dir: str) -> dict:
# TODO: Implement all security checks
safe_path = os.path.join(upload_dir, filename)
return {"ok": True, "safe_path": safe_path}
''',
"test_cases": [
{"fn": "validate_upload", "input": ["report.pdf", "application/pdf", 1024, "/uploads"], "expected_ok": True, "description": "Valid PDF accepted"},
{"fn": "validate_upload", "input": ["shell.php", "image/jpeg", 1024, "/uploads"], "expected_ok": False, "description": "PHP file rejected"},
{"fn": "validate_upload", "input": ["../../../etc/passwd", "text/plain", 100, "/uploads"], "expected_ok": False, "description": "Traversal filename rejected"},
{"fn": "validate_upload", "input": ["image.jpg", "text/html", 1024, "/uploads"], "expected_ok": False, "description": "MIME mismatch rejected"},
{"fn": "validate_upload", "input": ["huge.pdf", "application/pdf", 11 * 1024 * 1024, "/uploads"], "expected_ok": False, "description": "File too large rejected"},
{"fn": "validate_upload", "input": ["file.PHP", "image/jpeg", 1024, "/uploads"], "expected_ok": False, "description": "PHP with uppercase extension rejected"},
{"fn": "validate_upload", "input": ["photo.jpg", "image/jpeg", 5000, "/uploads"], "expected_ok": True, "description": "Valid JPEG accepted"},
],
"attack_type": "traversal",
"attack_config": {
"fn_name": "validate_upload",
"fn_args_template": ["{payload}", "image/jpeg", 1000, "/uploads"],
"check": lambda result, payload: (
isinstance(result, dict) and
(result.get("ok") is False or
(".." not in str(result.get("safe_path", "")) and
"/etc" not in str(result.get("safe_path", ""))))
),
},
"security_checks": [
{"type": "no_direct_join", "warn_on": ["os.path.join(upload_dir, filename)"]},
{"type": "uses_uuid", "recommended": ["uuid"]},
],
"naive_code": '''import os
def validate_upload(filename, content_type, file_size, upload_dir):
return {"ok": True, "safe_path": os.path.join(upload_dir, filename)}
''',
"optimal_code": '''import os
import re
import uuid
from pathlib import Path
ALLOWED_EXTENSIONS = {".jpg", ".jpeg", ".png", ".gif", ".pdf", ".txt", ".csv"}
BLOCKED_EXTENSIONS = {".php", ".py", ".sh", ".exe", ".js", ".html", ".htm", ".asp", ".jsp", ".rb", ".pl"}
ALLOWED_MIME_TYPES = {
".jpg": {"image/jpeg"}, ".jpeg": {"image/jpeg"},
".png": {"image/png"}, ".gif": {"image/gif"},
".pdf": {"application/pdf"},
".txt": {"text/plain"}, ".csv": {"text/csv", "text/plain"},
}
MAX_SIZE = 10 * 1024 * 1024 # 10MB
MAX_FILENAME_LEN = 100
def validate_upload(filename: str, content_type: str, file_size: int, upload_dir: str) -> dict:
"""Validates a file upload with full security checks."""
if not filename:
return {"ok": False, "reason": "Filename is empty"}
# 1. Null byte check
if "\\x00" in filename or "%00" in filename:
return {"ok": False, "reason": "Null byte in filename"}
# 2. Extract basename only — strip any path components
safe_name = Path(filename).name
if not safe_name:
return {"ok": False, "reason": "Invalid filename after stripping path"}
# 3. Block traversal sequences
if ".." in safe_name or "/" in safe_name or "\\\\" in safe_name:
return {"ok": False, "reason": "Path traversal in filename"}
# 4. Allow only safe characters
safe_name = re.sub(r"[^a-zA-Z0-9._\\-]", "_", safe_name)
# 5. Length check
if len(safe_name) > MAX_FILENAME_LEN:
return {"ok": False, "reason": f"Filename exceeds {MAX_FILENAME_LEN} characters"}
# 6. Extension check (case-insensitive)
ext = Path(safe_name).suffix.lower()
if ext in BLOCKED_EXTENSIONS:
return {"ok": False, "reason": f"Executable extension blocked: {ext}"}
if ext not in ALLOWED_EXTENSIONS:
return {"ok": False, "reason": f"Extension not allowed: {ext}"}
# 7. MIME type cross-check
allowed_mimes = ALLOWED_MIME_TYPES.get(ext, set())
if content_type not in allowed_mimes:
return {"ok": False, "reason": f"MIME type {content_type!r} not valid for {ext}"}
# 8. File size limit
if file_size > MAX_SIZE:
return {"ok": False, "reason": f"File too large: {file_size} bytes (max {MAX_SIZE})"}
# 9. Generate UUID-prefixed safe path
unique_name = f"{uuid.uuid4().hex}_{safe_name}"
safe_path = str(Path(upload_dir).resolve() / unique_name)
return {"ok": True, "safe_path": safe_path, "safe_filename": unique_name}
''',
}
|