diff --git a/docs/VULNERABILITY_CATALOG.md b/docs/VULNERABILITY_CATALOG.md index 619a9e8..90a24c9 100644 --- a/docs/VULNERABILITY_CATALOG.md +++ b/docs/VULNERABILITY_CATALOG.md @@ -7,11 +7,11 @@ from each file's header comment, so this page cannot drift from the source. ## Totals -- **Test cases:** 263 -- **Expected detections:** 263 -- **`VULNERABLE:` markers:** 375 (individual lines a scanner should flag) -- **`SAFE:` markers:** 280 (lines a scanner must not flag — the false-positive control group) -- **Languages:** 14 — c, cpp, dotenv, go, java, javascript, json, lua, python, ruby, rust, shell, swift, text +- **Test cases:** 278 +- **Expected detections:** 278 +- **`VULNERABLE:` markers:** 390 (individual lines a scanner should flag) +- **`SAFE:` markers:** 295 (lines a scanner must not flag — the false-positive control group) +- **Languages:** 15 — c, cpp, dotenv, go, java, javascript, json, lua, perl, python, ruby, rust, shell, swift, text - **CWE categories:** 102 — CWE-20, CWE-22, CWE-78, CWE-79, CWE-88, CWE-89, CWE-90, CWE-94, CWE-95, CWE-113, CWE-117, CWE-121, CWE-129, CWE-134, CWE-187, CWE-190, CWE-201, CWE-203, CWE-208, CWE-209, CWE-256, CWE-285, CWE-287, CWE-288, CWE-291, CWE-295, CWE-297, CWE-306, CWE-307, CWE-311, CWE-312, CWE-319, CWE-321, CWE-326, CWE-327, CWE-328, CWE-329, CWE-330, CWE-338, CWE-345, CWE-346, CWE-347, CWE-352, CWE-362, CWE-367, CWE-377, CWE-384, CWE-400, CWE-409, CWE-415, CWE-416, CWE-434, CWE-441, CWE-457, CWE-460, CWE-472, CWE-475, CWE-476, CWE-480, CWE-488, CWE-489, CWE-502, CWE-506, CWE-509, CWE-512, CWE-521, CWE-522, CWE-525, CWE-532, CWE-598, CWE-601, CWE-602, CWE-611, CWE-613, CWE-614, CWE-620, CWE-639, CWE-640, CWE-643, CWE-681, CWE-693, CWE-732, CWE-759, CWE-776, CWE-789, CWE-798, CWE-835, CWE-862, CWE-863, CWE-915, CWE-916, CWE-918, CWE-922, CWE-942, CWE-943, CWE-1004, CWE-1021, CWE-1236, CWE-1321, CWE-1333, CWE-1336, CWE-1357 ## How coverage is scored @@ -191,6 +191,26 @@ counts as a detection. See `docs/SCANNER_INTEGRATION.md`. | Predictable session token from math.random | [`weak-random-token.lua`](../vulns/lua/weak-random-token.lua) | CWE-338 | high | yes | 1 vuln / 1 safe | | World-writable permissions on exported data | [`world-writable-permissions.lua`](../vulns/lua/world-writable-permissions.lua) | CWE-732 | high | yes | 1 vuln / 1 safe | +## Perl + +| Test case | File | CWE | Severity | Expected | Markers | +|---|---|---|---|---|---| +| Dynamic Perl evaluation from request input | [`dynamic-eval-injection.pl`](../vulns/perl/dynamic-eval-injection.pl) | CWE-95 | critical | yes | 1 vuln / 1 safe | +| Hard-coded administrative password | [`hardcoded-password.pl`](../vulns/perl/hardcoded-password.pl) | CWE-798 | high | yes | 1 vuln / 1 safe | +| Log injection through unsanitized username | [`log-injection.pl`](../vulns/perl/log-injection.pl) | CWE-117 | medium | yes | 1 vuln / 1 safe | +| Open redirect through untrusted destination | [`open-redirect.pl`](../vulns/perl/open-redirect.pl) | CWE-601 | medium | yes | 1 vuln / 1 safe | +| Authentication using a truncated token comparison | [`partial-token-comparison.pl`](../vulns/perl/partial-token-comparison.pl) | CWE-187 | high | yes | 1 vuln / 1 safe | +| Path traversal in file download | [`path-traversal.pl`](../vulns/perl/path-traversal.pl) | CWE-22 | high | yes | 1 vuln / 1 safe | +| Predictable file in a shared temporary directory | [`predictable-temp-file.pl`](../vulns/perl/predictable-temp-file.pl) | CWE-377 | medium | yes | 1 vuln / 1 safe | +| SQL injection through interpolated query text | [`sql-injection.pl`](../vulns/perl/sql-injection.pl) | CWE-89 | critical | yes | 1 vuln / 1 safe | +| Server-side request forgery through user URL | [`ssrf-user-url.pl`](../vulns/perl/ssrf-user-url.pl) | CWE-918 | high | yes | 1 vuln / 1 safe | +| Internal stack trace returned to a client | [`stack-trace-exposure.pl`](../vulns/perl/stack-trace-exposure.pl) | CWE-209 | medium | yes | 1 vuln / 1 safe | +| Shell command injection through system | [`system-command-injection.pl`](../vulns/perl/system-command-injection.pl) | CWE-78 | critical | yes | 1 vuln / 1 safe | +| TLS hostname and certificate verification disabled | [`tls-verification-disabled.pl`](../vulns/perl/tls-verification-disabled.pl) | CWE-295 | high | yes | 1 vuln / 1 safe | +| Untrusted binary deserialization with Storable | [`unsafe-deserialization.pl`](../vulns/perl/unsafe-deserialization.pl) | CWE-502 | critical | yes | 1 vuln / 1 safe | +| Predictable session token from rand | [`weak-random-token.pl`](../vulns/perl/weak-random-token.pl) | CWE-338 | high | yes | 1 vuln / 1 safe | +| World-writable permissions on exported data | [`world-writable-permissions.pl`](../vulns/perl/world-writable-permissions.pl) | CWE-732 | high | yes | 1 vuln / 1 safe | + ## Python | Test case | File | CWE | Severity | Expected | Markers | diff --git a/vulns/VULNERABILITY_CATALOG.json b/vulns/VULNERABILITY_CATALOG.json index fc6a5f7..e6ae9d2 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": 263, - "expected_detections": 263, - "vulnerable_markers": 375, - "safe_markers": 280, + "test_cases": 278, + "expected_detections": 278, + "vulnerable_markers": 390, + "safe_markers": 295, "languages": [ "c", "cpp", @@ -15,6 +15,7 @@ "javascript", "json", "lua", + "perl", "python", "ruby", "rust", @@ -29,6 +30,7 @@ "java", "javascript", "lua", + "perl", "python", "ruby", "rust", @@ -3368,6 +3370,351 @@ 17 ] }, + { + "id": "perl-dynamic-eval-injection", + "file": "vulns/perl/dynamic-eval-injection.pl", + "title": "Dynamic Perl evaluation from request input", + "category": "perl", + "language": "perl", + "cwe": "CWE-95", + "cwes": [ + "CWE-95" + ], + "severity": "critical", + "expected_detection": true, + "description": "Request text is executed as Perl source code.", + "detection_target": "Tainted data passed to string eval.", + "safe_guard": "NEVER_RUN is false, so no string evaluation occurs.", + "attribution": "line", + "vulnerable_lines": [ + 19 + ], + "safe_lines": [ + 22 + ] + }, + { + "id": "perl-hardcoded-password", + "file": "vulns/perl/hardcoded-password.pl", + "title": "Hard-coded administrative password", + "category": "perl", + "language": "perl", + "cwe": "CWE-798", + "cwes": [ + "CWE-798" + ], + "severity": "high", + "expected_detection": true, + "description": "A synthetic administrative password is embedded in source.", + "detection_target": "Password variable assigned from a string literal.", + "safe_guard": "NEVER_RUN is false and the credential protects no account.", + "attribution": "line", + "vulnerable_lines": [ + 17 + ], + "safe_lines": [ + 20 + ] + }, + { + "id": "perl-log-injection", + "file": "vulns/perl/log-injection.pl", + "title": "Log injection through unsanitized username", + "category": "perl", + "language": "perl", + "cwe": "CWE-117", + "cwes": [ + "CWE-117" + ], + "severity": "medium", + "expected_detection": true, + "description": "Request text is written directly into a log line.", + "detection_target": "Tainted text passed to a logging sink without neutralization.", + "safe_guard": "NEVER_RUN is false, so no log entry is emitted.", + "attribution": "line", + "vulnerable_lines": [ + 19 + ], + "safe_lines": [ + 22 + ] + }, + { + "id": "perl-open-redirect", + "file": "vulns/perl/open-redirect.pl", + "title": "Open redirect through untrusted destination", + "category": "perl", + "language": "perl", + "cwe": "CWE-601", + "cwes": [ + "CWE-601" + ], + "severity": "medium", + "expected_detection": true, + "description": "A request destination is placed directly in a Location header.", + "detection_target": "Tainted URL used as a redirect destination.", + "safe_guard": "NEVER_RUN is false, so no HTTP response is emitted.", + "attribution": "line", + "vulnerable_lines": [ + 19 + ], + "safe_lines": [ + 23 + ] + }, + { + "id": "perl-partial-token-comparison", + "file": "vulns/perl/partial-token-comparison.pl", + "title": "Authentication using a truncated token comparison", + "category": "perl", + "language": "perl", + "cwe": "CWE-187", + "cwes": [ + "CWE-187" + ], + "severity": "high", + "expected_detection": true, + "description": "Only the first eight characters of a bearer token are compared.", + "detection_target": "substr used to compare partial credentials.", + "safe_guard": "NEVER_RUN is false, so no access decision occurs.", + "attribution": "line", + "vulnerable_lines": [ + 18 + ], + "safe_lines": [ + 20 + ] + }, + { + "id": "perl-path-traversal", + "file": "vulns/perl/path-traversal.pl", + "title": "Path traversal in file download", + "category": "perl", + "language": "perl", + "cwe": "CWE-22", + "cwes": [ + "CWE-22" + ], + "severity": "high", + "expected_detection": true, + "description": "A request filename is appended to a trusted directory without validation.", + "detection_target": "Tainted path passed to open.", + "safe_guard": "NEVER_RUN is false, so no file is opened.", + "attribution": "line", + "vulnerable_lines": [ + 19 + ], + "safe_lines": [ + 23 + ] + }, + { + "id": "perl-predictable-temp-file", + "file": "vulns/perl/predictable-temp-file.pl", + "title": "Predictable file in a shared temporary directory", + "category": "perl", + "language": "perl", + "cwe": "CWE-377", + "cwes": [ + "CWE-377" + ], + "severity": "medium", + "expected_detection": true, + "description": "Sensitive output uses a fixed path in /tmp.", + "detection_target": "Fixed temporary pathname passed to open.", + "safe_guard": "NEVER_RUN is false, so no file is created.", + "attribution": "line", + "vulnerable_lines": [ + 17 + ], + "safe_lines": [ + 21 + ] + }, + { + "id": "perl-sql-injection", + "file": "vulns/perl/sql-injection.pl", + "title": "SQL injection through interpolated query text", + "category": "perl", + "language": "perl", + "cwe": "CWE-89", + "cwes": [ + "CWE-89" + ], + "severity": "critical", + "expected_detection": true, + "description": "User input is embedded directly into a SQL statement.", + "detection_target": "Tainted value interpolated into a DBI query.", + "safe_guard": "NEVER_RUN is false, so no database query executes.", + "attribution": "line", + "vulnerable_lines": [ + 19 + ], + "safe_lines": [ + 21 + ] + }, + { + "id": "perl-ssrf-user-url", + "file": "vulns/perl/ssrf-user-url.pl", + "title": "Server-side request forgery through user URL", + "category": "perl", + "language": "perl", + "cwe": "CWE-918", + "cwes": [ + "CWE-918" + ], + "severity": "high", + "expected_detection": true, + "description": "A user-provided URL is fetched without host validation.", + "detection_target": "Tainted URL passed to an HTTP client.", + "safe_guard": "NEVER_RUN is false, so no network request occurs.", + "attribution": "line", + "vulnerable_lines": [ + 19 + ], + "safe_lines": [ + 23 + ] + }, + { + "id": "perl-stack-trace-exposure", + "file": "vulns/perl/stack-trace-exposure.pl", + "title": "Internal stack trace returned to a client", + "category": "perl", + "language": "perl", + "cwe": "CWE-209", + "cwes": [ + "CWE-209" + ], + "severity": "medium", + "expected_detection": true, + "description": "A detailed runtime trace is sent in an HTTP response.", + "detection_target": "Carp::longmess output reaching a client sink.", + "safe_guard": "NEVER_RUN is false, so no response is emitted.", + "attribution": "line", + "vulnerable_lines": [ + 18 + ], + "safe_lines": [ + 21 + ] + }, + { + "id": "perl-system-command-injection", + "file": "vulns/perl/system-command-injection.pl", + "title": "Shell command injection through system", + "category": "perl", + "language": "perl", + "cwe": "CWE-78", + "cwes": [ + "CWE-78" + ], + "severity": "critical", + "expected_detection": true, + "description": "User-controlled input is concatenated into a shell command.", + "detection_target": "Tainted input passed to the single-string form of system.", + "safe_guard": "NEVER_RUN is false, so no subprocess starts.", + "attribution": "line", + "vulnerable_lines": [ + 19 + ], + "safe_lines": [ + 22 + ] + }, + { + "id": "perl-tls-verification-disabled", + "file": "vulns/perl/tls-verification-disabled.pl", + "title": "TLS hostname and certificate verification disabled", + "category": "perl", + "language": "perl", + "cwe": "CWE-295", + "cwes": [ + "CWE-295" + ], + "severity": "high", + "expected_detection": true, + "description": "An HTTPS client disables peer and hostname verification.", + "detection_target": "ssl_opts configured with verify_hostname and SSL_verify_mode disabled.", + "safe_guard": "NEVER_RUN is false, so no client or request is created.", + "attribution": "line", + "vulnerable_lines": [ + 18 + ], + "safe_lines": [ + 23 + ] + }, + { + "id": "perl-unsafe-deserialization", + "file": "vulns/perl/unsafe-deserialization.pl", + "title": "Untrusted binary deserialization with Storable", + "category": "perl", + "language": "perl", + "cwe": "CWE-502", + "cwes": [ + "CWE-502" + ], + "severity": "critical", + "expected_detection": true, + "description": "Untrusted request bytes are deserialized into Perl objects.", + "detection_target": "Attacker-controlled bytes passed to Storable::thaw.", + "safe_guard": "NEVER_RUN is false, so no payload is decoded.", + "attribution": "line", + "vulnerable_lines": [ + 19 + ], + "safe_lines": [ + 22 + ] + }, + { + "id": "perl-weak-random-token", + "file": "vulns/perl/weak-random-token.pl", + "title": "Predictable session token from rand", + "category": "perl", + "language": "perl", + "cwe": "CWE-338", + "cwes": [ + "CWE-338" + ], + "severity": "high", + "expected_detection": true, + "description": "A security token is generated with Perl's non-cryptographic PRNG.", + "detection_target": "rand used for authentication or session material.", + "safe_guard": "NEVER_RUN is false, so no token is issued.", + "attribution": "line", + "vulnerable_lines": [ + 18 + ], + "safe_lines": [ + 21 + ] + }, + { + "id": "perl-world-writable-permissions", + "file": "vulns/perl/world-writable-permissions.pl", + "title": "World-writable permissions on exported data", + "category": "perl", + "language": "perl", + "cwe": "CWE-732", + "cwes": [ + "CWE-732" + ], + "severity": "high", + "expected_detection": true, + "description": "An exported file is assigned mode 0777.", + "detection_target": "chmod granting write access to every user.", + "safe_guard": "NEVER_RUN is false, so no permission is changed.", + "attribution": "line", + "vulnerable_lines": [ + 17 + ], + "safe_lines": [ + 19 + ] + }, { "id": "py-cleartext-sensitive-storage", "file": "vulns/python/cleartext-sensitive-storage.py", diff --git a/vulns/perl/dynamic-eval-injection.pl b/vulns/perl/dynamic-eval-injection.pl new file mode 100644 index 0000000..826f352 --- /dev/null +++ b/vulns/perl/dynamic-eval-injection.pl @@ -0,0 +1,24 @@ +# @id perl-dynamic-eval-injection +# @test-case Dynamic Perl evaluation from request input +# @cwe CWE-95 +# @severity critical +# @language perl +# @expected-detection true +# @description Request text is executed as Perl source code. +# @safe-guard NEVER_RUN is false, so no string evaluation occurs. +# @detection-target Tainted data passed to string eval. +# NEVER RUN IN PRODUCTION -- intentional scanner fixture. + +use strict; +use warnings; + +my $NEVER_RUN = 0; +if ($NEVER_RUN) { + my $request = { expression => 'synthetic' }; + my $source = $request->{expression}; # SOURCE + my $result = eval $source; # VULNERABLE: CWE-95 + + my %allowed = (status => sub { return 'ok' }); + my $action = $allowed{$request->{expression}}; # SAFE: allow-listed callback + $result = $action->() if $action; +} diff --git a/vulns/perl/hardcoded-password.pl b/vulns/perl/hardcoded-password.pl new file mode 100644 index 0000000..39a530c --- /dev/null +++ b/vulns/perl/hardcoded-password.pl @@ -0,0 +1,24 @@ +# @id perl-hardcoded-password +# @test-case Hard-coded administrative password +# @cwe CWE-798 +# @severity high +# @language perl +# @expected-detection true +# @description A synthetic administrative password is embedded in source. +# @safe-guard NEVER_RUN is false and the credential protects no account. +# @detection-target Password variable assigned from a string literal. +# NEVER RUN IN PRODUCTION -- intentional scanner fixture. + +use strict; +use warnings; + +my $NEVER_RUN = 0; +if ($NEVER_RUN) { + my $admin_password = 'TEST_ONLY_admin_password_123'; # VULNERABLE: CWE-798 + authenticate('admin', $admin_password); + + my $configured_password = $ENV{ADMIN_PASSWORD}; # SAFE: external secret source + authenticate('admin', $configured_password); +} + +sub authenticate { return 0 } diff --git a/vulns/perl/log-injection.pl b/vulns/perl/log-injection.pl new file mode 100644 index 0000000..da19cbe --- /dev/null +++ b/vulns/perl/log-injection.pl @@ -0,0 +1,23 @@ +# @id perl-log-injection +# @test-case Log injection through unsanitized username +# @cwe CWE-117 +# @severity medium +# @language perl +# @expected-detection true +# @description Request text is written directly into a log line. +# @safe-guard NEVER_RUN is false, so no log entry is emitted. +# @detection-target Tainted text passed to a logging sink without neutralization. +# NEVER RUN IN PRODUCTION -- intentional scanner fixture. + +use strict; +use warnings; + +my $NEVER_RUN = 0; +if ($NEVER_RUN) { + my ($logger, $request) = (undef, { username => 'synthetic' }); + my $username = $request->{username}; # SOURCE + $logger->info("login failed user=$username"); # VULNERABLE: CWE-117 + + (my $clean = $username) =~ s/[\r\n]/_/g; + $logger->info("login failed user=$clean"); # SAFE: line breaks neutralized +} diff --git a/vulns/perl/open-redirect.pl b/vulns/perl/open-redirect.pl new file mode 100644 index 0000000..e5dc6eb --- /dev/null +++ b/vulns/perl/open-redirect.pl @@ -0,0 +1,24 @@ +# @id perl-open-redirect +# @test-case Open redirect through untrusted destination +# @cwe CWE-601 +# @severity medium +# @language perl +# @expected-detection true +# @description A request destination is placed directly in a Location header. +# @safe-guard NEVER_RUN is false, so no HTTP response is emitted. +# @detection-target Tainted URL used as a redirect destination. +# NEVER RUN IN PRODUCTION -- intentional scanner fixture. + +use strict; +use warnings; + +my $NEVER_RUN = 0; +if ($NEVER_RUN) { + my $request = { next => 'synthetic' }; + my $next = $request->{next}; # SOURCE + print "Location: $next\r\n\r\n"; # VULNERABLE: CWE-601 + + my %routes = (home => '/', account => '/account'); + my $safe = $routes{$next} // '/'; + print "Location: $safe\r\n\r\n"; # SAFE: internal allow-list +} diff --git a/vulns/perl/partial-token-comparison.pl b/vulns/perl/partial-token-comparison.pl new file mode 100644 index 0000000..b6e766c --- /dev/null +++ b/vulns/perl/partial-token-comparison.pl @@ -0,0 +1,23 @@ +# @id perl-partial-token-comparison +# @test-case Authentication using a truncated token comparison +# @cwe CWE-187 +# @severity high +# @language perl +# @expected-detection true +# @description Only the first eight characters of a bearer token are compared. +# @safe-guard NEVER_RUN is false, so no access decision occurs. +# @detection-target substr used to compare partial credentials. +# NEVER RUN IN PRODUCTION -- intentional scanner fixture. + +use strict; +use warnings; + +my $NEVER_RUN = 0; +if ($NEVER_RUN) { + my ($provided, $expected) = ('synthetic-provided', 'synthetic-expected'); + grant_access() if substr($provided, 0, 8) eq substr($expected, 0, 8); # VULNERABLE: CWE-187 + + grant_access() if Security::Util::constant_time_eq($provided, $expected); # SAFE: full comparison +} + +sub grant_access { return } diff --git a/vulns/perl/path-traversal.pl b/vulns/perl/path-traversal.pl new file mode 100644 index 0000000..ade1158 --- /dev/null +++ b/vulns/perl/path-traversal.pl @@ -0,0 +1,25 @@ +# @id perl-path-traversal +# @test-case Path traversal in file download +# @cwe CWE-22 +# @severity high +# @language perl +# @expected-detection true +# @description A request filename is appended to a trusted directory without validation. +# @safe-guard NEVER_RUN is false, so no file is opened. +# @detection-target Tainted path passed to open. +# NEVER RUN IN PRODUCTION -- intentional scanner fixture. + +use strict; +use warnings; + +my $NEVER_RUN = 0; +if ($NEVER_RUN) { + my $request = { name => 'synthetic' }; + my $name = $request->{name}; # SOURCE + open my $unsafe, '<', "/srv/public/$name" or die $!; # VULNERABLE: CWE-22 + close $unsafe; + + die 'invalid basename' unless $name =~ /\A[\w.-]+\z/; + open my $safe, '<', "/srv/public/$name" or die $!; # SAFE: basename allow-list + close $safe; +} diff --git a/vulns/perl/predictable-temp-file.pl b/vulns/perl/predictable-temp-file.pl new file mode 100644 index 0000000..2e9251a --- /dev/null +++ b/vulns/perl/predictable-temp-file.pl @@ -0,0 +1,24 @@ +# @id perl-predictable-temp-file +# @test-case Predictable file in a shared temporary directory +# @cwe CWE-377 +# @severity medium +# @language perl +# @expected-detection true +# @description Sensitive output uses a fixed path in /tmp. +# @safe-guard NEVER_RUN is false, so no file is created. +# @detection-target Fixed temporary pathname passed to open. +# NEVER RUN IN PRODUCTION -- intentional scanner fixture. + +use strict; +use warnings; + +my $NEVER_RUN = 0; +if ($NEVER_RUN) { + open my $unsafe, '>', '/tmp/account-report.txt' or die $!; # VULNERABLE: CWE-377 + print {$unsafe} 'synthetic report'; + close $unsafe; + + my ($safe, $safe_path) = File::Temp::tempfile('report-XXXXXX', TMPDIR => 1); # SAFE: exclusive unique file + print {$safe} 'synthetic report'; + close $safe; +} diff --git a/vulns/perl/sql-injection.pl b/vulns/perl/sql-injection.pl new file mode 100644 index 0000000..018a9c5 --- /dev/null +++ b/vulns/perl/sql-injection.pl @@ -0,0 +1,22 @@ +# @id perl-sql-injection +# @test-case SQL injection through interpolated query text +# @cwe CWE-89 +# @severity critical +# @language perl +# @expected-detection true +# @description User input is embedded directly into a SQL statement. +# @safe-guard NEVER_RUN is false, so no database query executes. +# @detection-target Tainted value interpolated into a DBI query. +# NEVER RUN IN PRODUCTION -- intentional scanner fixture. + +use strict; +use warnings; + +my $NEVER_RUN = 0; +if ($NEVER_RUN) { + my ($dbh, $request) = (undef, { email => 'synthetic' }); + my $email = $request->{email}; # SOURCE + $dbh->selectall_arrayref("SELECT * FROM users WHERE email = '$email'"); # VULNERABLE: CWE-89 + + $dbh->selectall_arrayref('SELECT * FROM users WHERE email = ?', {}, $email); # SAFE: placeholder +} diff --git a/vulns/perl/ssrf-user-url.pl b/vulns/perl/ssrf-user-url.pl new file mode 100644 index 0000000..68250cd --- /dev/null +++ b/vulns/perl/ssrf-user-url.pl @@ -0,0 +1,24 @@ +# @id perl-ssrf-user-url +# @test-case Server-side request forgery through user URL +# @cwe CWE-918 +# @severity high +# @language perl +# @expected-detection true +# @description A user-provided URL is fetched without host validation. +# @safe-guard NEVER_RUN is false, so no network request occurs. +# @detection-target Tainted URL passed to an HTTP client. +# NEVER RUN IN PRODUCTION -- intentional scanner fixture. + +use strict; +use warnings; + +my $NEVER_RUN = 0; +if ($NEVER_RUN) { + my ($ua, $request) = (LWP::UserAgent->new, { url => 'synthetic' }); + my $target = $request->{url}; # SOURCE + $ua->get($target); # VULNERABLE: CWE-918 + + my ($report_id) = $target =~ /\Areport-(\d+)\z/; + die 'invalid report id' unless defined $report_id; + $ua->get("https://reports.invalid/$report_id"); # SAFE: fixed .invalid host +} diff --git a/vulns/perl/stack-trace-exposure.pl b/vulns/perl/stack-trace-exposure.pl new file mode 100644 index 0000000..cd3be95 --- /dev/null +++ b/vulns/perl/stack-trace-exposure.pl @@ -0,0 +1,22 @@ +# @id perl-stack-trace-exposure +# @test-case Internal stack trace returned to a client +# @cwe CWE-209 +# @severity medium +# @language perl +# @expected-detection true +# @description A detailed runtime trace is sent in an HTTP response. +# @safe-guard NEVER_RUN is false, so no response is emitted. +# @detection-target Carp::longmess output reaching a client sink. +# NEVER RUN IN PRODUCTION -- intentional scanner fixture. + +use strict; +use warnings; + +my $NEVER_RUN = 0; +if ($NEVER_RUN) { + my ($response, $logger, $error) = (undef, undef, 'synthetic failure'); + $response->send(500, Carp::longmess($error)); # VULNERABLE: CWE-209 + + $logger->error(Carp::longmess($error)); + $response->send(500, 'internal error'); # SAFE: generic client message +} diff --git a/vulns/perl/system-command-injection.pl b/vulns/perl/system-command-injection.pl new file mode 100644 index 0000000..affd46a --- /dev/null +++ b/vulns/perl/system-command-injection.pl @@ -0,0 +1,23 @@ +# @id perl-system-command-injection +# @test-case Shell command injection through system +# @cwe CWE-78 +# @severity critical +# @language perl +# @expected-detection true +# @description User-controlled input is concatenated into a shell command. +# @safe-guard NEVER_RUN is false, so no subprocess starts. +# @detection-target Tainted input passed to the single-string form of system. +# NEVER RUN IN PRODUCTION -- intentional scanner fixture. + +use strict; +use warnings; + +my $NEVER_RUN = 0; +if ($NEVER_RUN) { + my $request = { filename => 'synthetic' }; + my $filename = $request->{filename}; # SOURCE: attacker-controlled + system("cat " . $filename); # VULNERABLE: CWE-78 + + die 'invalid filename' unless $filename =~ /\A[\w.-]+\z/; + system('cat', '--', "/srv/reports/$filename"); # SAFE: list form, validated name +} diff --git a/vulns/perl/tls-verification-disabled.pl b/vulns/perl/tls-verification-disabled.pl new file mode 100644 index 0000000..eacf637 --- /dev/null +++ b/vulns/perl/tls-verification-disabled.pl @@ -0,0 +1,26 @@ +# @id perl-tls-verification-disabled +# @test-case TLS hostname and certificate verification disabled +# @cwe CWE-295 +# @severity high +# @language perl +# @expected-detection true +# @description An HTTPS client disables peer and hostname verification. +# @safe-guard NEVER_RUN is false, so no client or request is created. +# @detection-target ssl_opts configured with verify_hostname and SSL_verify_mode disabled. +# NEVER RUN IN PRODUCTION -- intentional scanner fixture. + +use strict; +use warnings; + +my $NEVER_RUN = 0; +if ($NEVER_RUN) { + my $unsafe = LWP::UserAgent->new( + ssl_opts => { verify_hostname => 0, SSL_verify_mode => 0 }, # VULNERABLE: CWE-295 + ); + $unsafe->get('https://service.invalid/status'); + + my $safe = LWP::UserAgent->new( + ssl_opts => { verify_hostname => 1 }, # SAFE: verification enabled + ); + $safe->get('https://service.invalid/status'); +} diff --git a/vulns/perl/unsafe-deserialization.pl b/vulns/perl/unsafe-deserialization.pl new file mode 100644 index 0000000..24aa822 --- /dev/null +++ b/vulns/perl/unsafe-deserialization.pl @@ -0,0 +1,26 @@ +# @id perl-unsafe-deserialization +# @test-case Untrusted binary deserialization with Storable +# @cwe CWE-502 +# @severity critical +# @language perl +# @expected-detection true +# @description Untrusted request bytes are deserialized into Perl objects. +# @safe-guard NEVER_RUN is false, so no payload is decoded. +# @detection-target Attacker-controlled bytes passed to Storable::thaw. +# NEVER RUN IN PRODUCTION -- intentional scanner fixture. + +use strict; +use warnings; + +my $NEVER_RUN = 0; +if ($NEVER_RUN) { + my $request = { body => 'synthetic' }; + my $payload = $request->{body}; # SOURCE + my $value = Storable::thaw($payload); # VULNERABLE: CWE-502 + consume($value); + + my $safe_value = JSON::PP::decode_json($payload); # SAFE: data-only JSON parser + consume($safe_value); +} + +sub consume { return } diff --git a/vulns/perl/weak-random-token.pl b/vulns/perl/weak-random-token.pl new file mode 100644 index 0000000..f6112c4 --- /dev/null +++ b/vulns/perl/weak-random-token.pl @@ -0,0 +1,25 @@ +# @id perl-weak-random-token +# @test-case Predictable session token from rand +# @cwe CWE-338 +# @severity high +# @language perl +# @expected-detection true +# @description A security token is generated with Perl's non-cryptographic PRNG. +# @safe-guard NEVER_RUN is false, so no token is issued. +# @detection-target rand used for authentication or session material. +# NEVER RUN IN PRODUCTION -- intentional scanner fixture. + +use strict; +use warnings; + +my $NEVER_RUN = 0; +if ($NEVER_RUN) { + srand(time); + my $session_token = int(rand(900000)) + 100000; # VULNERABLE: CWE-338 + issue_session($session_token); + + my $secure_token = Crypt::URandom::urandom(32); # SAFE: OS-backed random bytes + issue_session($secure_token); +} + +sub issue_session { return } diff --git a/vulns/perl/world-writable-permissions.pl b/vulns/perl/world-writable-permissions.pl new file mode 100644 index 0000000..5f24f06 --- /dev/null +++ b/vulns/perl/world-writable-permissions.pl @@ -0,0 +1,20 @@ +# @id perl-world-writable-permissions +# @test-case World-writable permissions on exported data +# @cwe CWE-732 +# @severity high +# @language perl +# @expected-detection true +# @description An exported file is assigned mode 0777. +# @safe-guard NEVER_RUN is false, so no permission is changed. +# @detection-target chmod granting write access to every user. +# NEVER RUN IN PRODUCTION -- intentional scanner fixture. + +use strict; +use warnings; + +my $NEVER_RUN = 0; +if ($NEVER_RUN) { + chmod 0777, '/srv/export/report.json'; # VULNERABLE: CWE-732 + + chmod 0600, '/srv/export/report.json'; # SAFE: owner-only access +}