diff --git a/docs/VULNERABILITY_CATALOG.md b/docs/VULNERABILITY_CATALOG.md index 5763d34..d017fcd 100644 --- a/docs/VULNERABILITY_CATALOG.md +++ b/docs/VULNERABILITY_CATALOG.md @@ -7,12 +7,12 @@ from each file's header comment, so this page cannot drift from the source. ## Totals -- **Test cases:** 62 -- **Expected detections:** 62 -- **`VULNERABLE:` markers:** 123 (individual lines a scanner should flag) -- **`SAFE:` markers:** 73 (lines a scanner must not flag — the false-positive control group) +- **Test cases:** 63 +- **Expected detections:** 63 +- **`VULNERABLE:` markers:** 126 (individual lines a scanner should flag) +- **`SAFE:` markers:** 75 (lines a scanner must not flag — the false-positive control group) - **Languages:** 8 — dotenv, go, java, javascript, json, python, ruby, text -- **CWE categories:** 46 — CWE-20, CWE-22, CWE-78, CWE-79, CWE-89, CWE-90, CWE-95, CWE-113, CWE-117, CWE-190, CWE-201, CWE-209, CWE-256, CWE-295, CWE-321, CWE-327, CWE-330, CWE-338, CWE-346, CWE-347, CWE-352, CWE-362, CWE-377, CWE-384, CWE-489, CWE-502, CWE-506, CWE-532, CWE-601, CWE-611, CWE-614, CWE-639, CWE-643, CWE-681, CWE-759, CWE-798, CWE-862, CWE-915, CWE-918, CWE-942, CWE-943, CWE-1236, CWE-1321, CWE-1333, CWE-1336, CWE-1357 +- **CWE categories:** 47 — CWE-20, CWE-22, CWE-78, CWE-79, CWE-89, CWE-90, CWE-95, CWE-99, CWE-113, CWE-117, CWE-190, CWE-201, CWE-209, CWE-256, CWE-295, CWE-321, CWE-327, CWE-330, CWE-338, CWE-346, CWE-347, CWE-352, CWE-362, CWE-377, CWE-384, CWE-489, CWE-502, CWE-506, CWE-532, CWE-601, CWE-611, CWE-614, CWE-639, CWE-643, CWE-681, CWE-759, CWE-798, CWE-862, CWE-915, CWE-918, CWE-942, CWE-943, CWE-1236, CWE-1321, CWE-1333, CWE-1336, CWE-1357 ## How coverage is scored @@ -97,6 +97,7 @@ counts as a detection. See `docs/SCANNER_INTEGRATION.md`. | Test case | File | CWE | Severity | Expected | Markers | |---|---|---|---|---|---| +| Resource injection via file path manipulation in archive extraction | [`cwe-99-ruby.rb`](../vulns/ruby/cwe-99-ruby.rb) | CWE-99 | high | yes | 3 vuln / 2 safe | | Code and command injection via eval / send / backticks | [`rce-eval.rb`](../vulns/ruby/rce-eval.rb) | CWE-95 | critical | yes | 3 vuln / 1 safe | | SQL injection via string interpolation in ActiveRecord | [`sqli-string-interpolation.rb`](../vulns/ruby/sqli-string-interpolation.rb) | CWE-89 | critical | yes | 3 vuln / 1 safe | | XSS via html_safe / raw on untrusted input | [`xss-erb-html-safe.rb`](../vulns/ruby/xss-erb-html-safe.rb) | CWE-79 | high | yes | 3 vuln / 1 safe | diff --git a/vulns/VULNERABILITY_CATALOG.json b/vulns/VULNERABILITY_CATALOG.json index 153fdaf..09f76a2 100644 --- a/vulns/VULNERABILITY_CATALOG.json +++ b/vulns/VULNERABILITY_CATALOG.json @@ -2,10 +2,10 @@ "schema": "threatcrush-testbed-catalog/1", "note": "Generated by scripts/generate-catalog.py \u2014 do not edit by hand.", "totals": { - "test_cases": 62, - "expected_detections": 62, - "vulnerable_markers": 123, - "safe_markers": 73, + "test_cases": 63, + "expected_detections": 63, + "vulnerable_markers": 126, + "safe_markers": 75, "languages": [ "dotenv", "go", @@ -33,6 +33,7 @@ "CWE-89", "CWE-90", "CWE-95", + "CWE-99", "CWE-113", "CWE-117", "CWE-190", @@ -1305,6 +1306,32 @@ 53 ] }, + { + "id": "rb-cwe99-resource-injection", + "file": "vulns/ruby/cwe-99-ruby.rb", + "title": "Resource injection via file path manipulation in archive extraction", + "category": "ruby", + "language": "ruby", + "cwe": "CWE-99", + "cwes": [ + "CWE-99" + ], + "severity": "high", + "expected_detection": true, + "description": "User input is used to construct a file path for archive extraction", + "detection_target": "Taint flow from params into File.open / File.join / Pathname /", + "safe_guard": "Every payload sits inside `if false` \u2014 unreachable dead code.", + "attribution": "line", + "vulnerable_lines": [ + 25, + 36, + 48 + ], + "safe_lines": [ + 59, + 74 + ] + }, { "id": "rb-rce-eval", "file": "vulns/ruby/rce-eval.rb", diff --git a/vulns/ruby/cwe-99-ruby.rb b/vulns/ruby/cwe-99-ruby.rb new file mode 100644 index 0000000..ac4d6c2 --- /dev/null +++ b/vulns/ruby/cwe-99-ruby.rb @@ -0,0 +1,83 @@ +# @id rb-cwe99-resource-injection +# @test-case Resource injection via file path manipulation in archive extraction +# @cwe CWE-99 +# @severity high +# @language ruby +# @expected-detection true +# @description User input is used to construct a file path for archive extraction +# without proper validation, allowing path traversal and arbitrary +# file write via zip-slip style attacks. +# @safe-guard Every payload sits inside `if false` — unreachable dead code. +# @detection-target Taint flow from params into File.open / File.join / Pathname / +# Zip::File / tar extraction paths / unvalidated path construction. +# +# NEVER RUN IN PRODUCTION — intentional test case for scanner validation. + +# rubocop:disable all +module Vulns + module ResourceInjection + module_function + + def extract_vulnerable(params) + if false + archive_path = params[:archive_path] # SOURCE: attacker-controlled + entry_name = params[:entry_name] # SOURCE: attacker-controlled + destination = File.join('/tmp/extract', entry_name) # VULNERABLE: CWE-99 sink + File.open(destination, 'wb') do |file| + file.write("extracted content") + end + end + end + + def path_join_vulnerable(params) + if false + base_dir = params[:base_dir] # SOURCE + file_name = params[:file_name] # SOURCE + full_path = File.join(base_dir, file_name) # VULNERABLE: CWE-99 sink + File.read(full_path) + end + end + + def zip_extract_vulnerable(params) + if false + zip_path = params[:zip_path] # SOURCE + entry_name = params[:entry_name] # SOURCE + require 'zip' + Zip::File.open(zip_path) do |zip_file| + entry = zip_file.find_entry(entry_name) + entry.extract(File.join('/tmp/unzip', entry_name)) # VULNERABLE: CWE-99 sink + end + end + end + + # Safe counterpart — the scanner should NOT flag this. + # @expected-detection false + def extract_safe(params) + if false + archive_path = params[:archive_path] + entry_name = params[:entry_name] + # SAFE: sanitize entry name to prevent path traversal + safe_name = File.basename(entry_name) + destination = File.join('/tmp/extract', safe_name) + File.open(destination, 'wb') do |file| + file.write("extracted content") + end + end + end + + # Safe counterpart — the scanner should NOT flag this. + # @expected-detection false + def path_join_safe(params) + if false + base_dir = params[:base_dir] + file_name = params[:file_name] + # SAFE: validate base directory is allowed + allowed_base = '/var/data/example.com' + return nil unless base_dir == allowed_base + full_path = File.join(base_dir, File.basename(file_name)) + File.read(full_path) + end + end + end +end +# rubocop:enable all \ No newline at end of file