@intentsolutionsio/penetration-tester 2.0.0 → 3.0.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/plugin.json +8 -3
- package/README.md +8 -0
- package/commands/pentest.md +5 -0
- package/package.json +8 -3
- package/skills/analyzing-tls-config/SKILL.md +221 -0
- package/skills/analyzing-tls-config/references/AUTHORIZATION.md +133 -0
- package/skills/analyzing-tls-config/references/PLAYBOOK.md +267 -0
- package/skills/analyzing-tls-config/references/THEORY.md +128 -0
- package/skills/analyzing-tls-config/scripts/analyze_tls.py +415 -0
- package/skills/auditing-cors-policy/SKILL.md +186 -0
- package/skills/auditing-cors-policy/references/PLAYBOOK.md +220 -0
- package/skills/auditing-cors-policy/references/THEORY.md +142 -0
- package/skills/auditing-cors-policy/scripts/audit_cors.py +350 -0
- package/skills/auditing-npm-dependencies/SKILL.md +254 -0
- package/skills/auditing-npm-dependencies/references/PLAYBOOK.md +175 -0
- package/skills/auditing-npm-dependencies/references/THEORY.md +122 -0
- package/skills/auditing-npm-dependencies/scripts/audit_npm.py +408 -0
- package/skills/auditing-python-dependencies/SKILL.md +251 -0
- package/skills/auditing-python-dependencies/references/PLAYBOOK.md +193 -0
- package/skills/auditing-python-dependencies/references/THEORY.md +122 -0
- package/skills/auditing-python-dependencies/scripts/audit_python.py +459 -0
- package/skills/checking-http-security-headers/SKILL.md +176 -0
- package/skills/checking-http-security-headers/references/PLAYBOOK.md +212 -0
- package/skills/checking-http-security-headers/references/THEORY.md +137 -0
- package/skills/checking-http-security-headers/scripts/check_headers.py +362 -0
- package/skills/checking-license-compliance/SKILL.md +225 -0
- package/skills/checking-license-compliance/references/PLAYBOOK.md +161 -0
- package/skills/checking-license-compliance/references/THEORY.md +152 -0
- package/skills/checking-license-compliance/scripts/check_licenses.py +461 -0
- package/skills/composing-vulnerability-report/SKILL.md +212 -0
- package/skills/composing-vulnerability-report/references/PLAYBOOK.md +180 -0
- package/skills/composing-vulnerability-report/references/THEORY.md +178 -0
- package/skills/composing-vulnerability-report/scripts/compose_report.py +396 -0
- package/skills/confirming-pentest-authorization/SKILL.md +247 -0
- package/skills/confirming-pentest-authorization/references/PLAYBOOK.md +189 -0
- package/skills/confirming-pentest-authorization/references/THEORY.md +167 -0
- package/skills/confirming-pentest-authorization/scripts/check_authorization.py +457 -0
- package/skills/defining-pentest-scope/SKILL.md +227 -0
- package/skills/defining-pentest-scope/references/PLAYBOOK.md +238 -0
- package/skills/defining-pentest-scope/references/THEORY.md +170 -0
- package/skills/defining-pentest-scope/scripts/define_scope.py +472 -0
- package/skills/detecting-command-injection-patterns/SKILL.md +144 -0
- package/skills/detecting-command-injection-patterns/references/PLAYBOOK.md +302 -0
- package/skills/detecting-command-injection-patterns/references/THEORY.md +206 -0
- package/skills/detecting-command-injection-patterns/scripts/scan_cmdi.py +290 -0
- package/skills/detecting-debug-endpoints/SKILL.md +207 -0
- package/skills/detecting-debug-endpoints/references/PLAYBOOK.md +402 -0
- package/skills/detecting-debug-endpoints/references/THEORY.md +218 -0
- package/skills/detecting-debug-endpoints/scripts/probe_debug.py +518 -0
- package/skills/detecting-directory-listing/SKILL.md +206 -0
- package/skills/detecting-directory-listing/references/PLAYBOOK.md +277 -0
- package/skills/detecting-directory-listing/references/THEORY.md +203 -0
- package/skills/detecting-directory-listing/scripts/probe_directory_listing.py +180 -0
- package/skills/detecting-eval-exec-usage/SKILL.md +128 -0
- package/skills/detecting-eval-exec-usage/references/PLAYBOOK.md +306 -0
- package/skills/detecting-eval-exec-usage/references/THEORY.md +159 -0
- package/skills/detecting-eval-exec-usage/scripts/scan_eval.py +223 -0
- package/skills/detecting-exposed-secrets-files/SKILL.md +179 -0
- package/skills/detecting-exposed-secrets-files/references/PLAYBOOK.md +274 -0
- package/skills/detecting-exposed-secrets-files/references/THEORY.md +174 -0
- package/skills/detecting-exposed-secrets-files/scripts/probe_secrets.py +207 -0
- package/skills/detecting-insecure-deserialization/SKILL.md +148 -0
- package/skills/detecting-insecure-deserialization/references/PLAYBOOK.md +333 -0
- package/skills/detecting-insecure-deserialization/references/THEORY.md +199 -0
- package/skills/detecting-insecure-deserialization/scripts/scan_deserialization.py +250 -0
- package/skills/detecting-sql-injection-patterns/SKILL.md +161 -0
- package/skills/detecting-sql-injection-patterns/references/PLAYBOOK.md +317 -0
- package/skills/detecting-sql-injection-patterns/references/THEORY.md +261 -0
- package/skills/detecting-sql-injection-patterns/scripts/scan_sqli.py +354 -0
- package/skills/detecting-ssl-cert-issues/SKILL.md +182 -0
- package/skills/detecting-ssl-cert-issues/references/PLAYBOOK.md +203 -0
- package/skills/detecting-ssl-cert-issues/references/THEORY.md +133 -0
- package/skills/detecting-ssl-cert-issues/scripts/check_cert_chain.py +481 -0
- package/skills/detecting-weak-cryptography/SKILL.md +147 -0
- package/skills/detecting-weak-cryptography/references/PLAYBOOK.md +466 -0
- package/skills/detecting-weak-cryptography/references/THEORY.md +194 -0
- package/skills/detecting-weak-cryptography/scripts/scan_weak_crypto.py +417 -0
- package/skills/fingerprinting-server-software/SKILL.md +191 -0
- package/skills/fingerprinting-server-software/references/PLAYBOOK.md +337 -0
- package/skills/fingerprinting-server-software/references/THEORY.md +183 -0
- package/skills/fingerprinting-server-software/scripts/fingerprint_server.py +347 -0
- package/skills/generating-executive-summary/SKILL.md +261 -0
- package/skills/generating-executive-summary/references/PLAYBOOK.md +201 -0
- package/skills/generating-executive-summary/references/THEORY.md +195 -0
- package/skills/generating-executive-summary/scripts/exec_summary.py +538 -0
- package/skills/mapping-findings-to-owasp-top10/SKILL.md +235 -0
- package/skills/mapping-findings-to-owasp-top10/references/PLAYBOOK.md +193 -0
- package/skills/mapping-findings-to-owasp-top10/references/THEORY.md +160 -0
- package/skills/mapping-findings-to-owasp-top10/scripts/map_owasp.py +540 -0
- package/skills/performing-penetration-testing/SKILL.md +282 -190
- package/skills/performing-penetration-testing/references/OWASP_TOP_10.md +22 -0
- package/skills/performing-penetration-testing/references/REMEDIATION_PLAYBOOK.md +46 -0
- package/skills/performing-penetration-testing/references/SECURITY_HEADERS.md +41 -0
- package/skills/performing-penetration-testing/scripts/code_security_scanner.py +144 -79
- package/skills/performing-penetration-testing/scripts/dependency_auditor.py +116 -93
- package/skills/performing-penetration-testing/scripts/security_scanner.py +574 -446
- package/skills/probing-dangerous-http-methods/SKILL.md +182 -0
- package/skills/probing-dangerous-http-methods/references/PLAYBOOK.md +234 -0
- package/skills/probing-dangerous-http-methods/references/THEORY.md +145 -0
- package/skills/probing-dangerous-http-methods/scripts/probe_methods.py +263 -0
- package/skills/recording-pentest-engagement/SKILL.md +253 -0
- package/skills/recording-pentest-engagement/references/PLAYBOOK.md +203 -0
- package/skills/recording-pentest-engagement/references/THEORY.md +195 -0
- package/skills/recording-pentest-engagement/scripts/record_engagement.py +461 -0
- package/skills/scanning-for-hardcoded-secrets/SKILL.md +215 -0
- package/skills/scanning-for-hardcoded-secrets/references/PLAYBOOK.md +325 -0
- package/skills/scanning-for-hardcoded-secrets/references/THEORY.md +175 -0
- package/skills/scanning-for-hardcoded-secrets/scripts/scan_secrets.py +395 -0
- package/skills/tracing-transitive-vulnerabilities/SKILL.md +235 -0
- package/skills/tracing-transitive-vulnerabilities/references/PLAYBOOK.md +233 -0
- package/skills/tracing-transitive-vulnerabilities/references/THEORY.md +138 -0
- package/skills/tracing-transitive-vulnerabilities/scripts/trace_vulns.py +484 -0
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Directory-listing probe.
|
|
3
|
+
|
|
4
|
+
Companion to skill `detecting-directory-listing`. For each candidate
|
|
5
|
+
directory path, appends a trailing slash and sends a GET. If the
|
|
6
|
+
response is 200 and the body matches a framework-specific autoindex
|
|
7
|
+
fingerprint, it's a finding.
|
|
8
|
+
|
|
9
|
+
References:
|
|
10
|
+
OWASP WSTG-CONF-04 Review Old Backup and Unreferenced Files
|
|
11
|
+
CWE-548 Exposure of Information Through Directory Listing
|
|
12
|
+
nginx autoindex docs, Apache mod_autoindex docs
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
import argparse
|
|
18
|
+
import re
|
|
19
|
+
import sys
|
|
20
|
+
from pathlib import Path
|
|
21
|
+
|
|
22
|
+
_PLUGIN_ROOT = Path(__file__).resolve().parents[3]
|
|
23
|
+
if str(_PLUGIN_ROOT) not in sys.path:
|
|
24
|
+
sys.path.insert(0, str(_PLUGIN_ROOT))
|
|
25
|
+
|
|
26
|
+
from lib.authz_check import require_authorization # noqa: E402
|
|
27
|
+
from lib.finding import Finding, Severity # noqa: E402
|
|
28
|
+
from lib.http_client import make_session, safe_get # noqa: E402
|
|
29
|
+
from lib.report import emit, exit_code # noqa: E402
|
|
30
|
+
|
|
31
|
+
SKILL_ID = "detecting-directory-listing"
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
# Probe set. Each entry: (path, severity, control)
|
|
35
|
+
PROBES = [
|
|
36
|
+
# CRITICAL — config + VCS directories
|
|
37
|
+
("config/", Severity.CRITICAL, "NIST 800-53 SC-28"),
|
|
38
|
+
("conf/", Severity.CRITICAL, "NIST 800-53 SC-28"),
|
|
39
|
+
(".config/", Severity.CRITICAL, "NIST 800-53 SC-28"),
|
|
40
|
+
(".git/", Severity.CRITICAL, "NIST 800-53 SC-28"),
|
|
41
|
+
(".git/objects/", Severity.CRITICAL, "NIST 800-53 SC-28"),
|
|
42
|
+
(".svn/", Severity.CRITICAL, "NIST 800-53 SC-28"),
|
|
43
|
+
(".hg/", Severity.CRITICAL, "NIST 800-53 SC-28"),
|
|
44
|
+
# HIGH — backup / upload / log / dump dirs
|
|
45
|
+
("backup/", Severity.HIGH, "CWE-548"),
|
|
46
|
+
("backups/", Severity.HIGH, "CWE-548"),
|
|
47
|
+
("uploads/", Severity.HIGH, "CWE-548"),
|
|
48
|
+
("upload/", Severity.HIGH, "CWE-548"),
|
|
49
|
+
("logs/", Severity.HIGH, "CWE-548"),
|
|
50
|
+
("log/", Severity.HIGH, "CWE-548"),
|
|
51
|
+
("dump/", Severity.HIGH, "CWE-548"),
|
|
52
|
+
("dumps/", Severity.HIGH, "CWE-548"),
|
|
53
|
+
("tmp/", Severity.HIGH, "CWE-548"),
|
|
54
|
+
("temp/", Severity.HIGH, "CWE-548"),
|
|
55
|
+
("cache/", Severity.HIGH, "CWE-548"),
|
|
56
|
+
("data/", Severity.HIGH, "CWE-548"),
|
|
57
|
+
("private/", Severity.HIGH, "CWE-548"),
|
|
58
|
+
("internal/", Severity.HIGH, "CWE-548"),
|
|
59
|
+
("storage/", Severity.HIGH, "CWE-548"),
|
|
60
|
+
("var/", Severity.HIGH, "CWE-548"),
|
|
61
|
+
("files/", Severity.HIGH, "CWE-548"),
|
|
62
|
+
# MEDIUM — asset / public-ish dirs (file enumeration enabled)
|
|
63
|
+
("assets/", Severity.MEDIUM, "CWE-548"),
|
|
64
|
+
("static/", Severity.MEDIUM, "CWE-548"),
|
|
65
|
+
("public/", Severity.MEDIUM, "CWE-548"),
|
|
66
|
+
("media/", Severity.MEDIUM, "CWE-548"),
|
|
67
|
+
("images/", Severity.MEDIUM, "CWE-548"),
|
|
68
|
+
("img/", Severity.MEDIUM, "CWE-548"),
|
|
69
|
+
("downloads/", Severity.MEDIUM, "CWE-548"),
|
|
70
|
+
("docs/", Severity.MEDIUM, "CWE-548"),
|
|
71
|
+
("documentation/", Severity.MEDIUM, "CWE-548"),
|
|
72
|
+
("vendor/", Severity.MEDIUM, "CWE-548"),
|
|
73
|
+
("node_modules/", Severity.HIGH, "CWE-548"), # higher: enables specific version-CVE lookup
|
|
74
|
+
("bower_components/", Severity.HIGH, "CWE-548"),
|
|
75
|
+
# MEDIUM — generic root
|
|
76
|
+
("", Severity.MEDIUM, "OWASP A05:2021"), # root itself
|
|
77
|
+
]
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
# Framework-specific autoindex fingerprints applied to first 4 KiB of body
|
|
81
|
+
AUTOINDEX_PATTERNS = [
|
|
82
|
+
(r"<title>Index of /", "Apache mod_autoindex"),
|
|
83
|
+
(r"<h1>Index of /", "Apache mod_autoindex / nginx fancyindex"),
|
|
84
|
+
(r"<title>Directory listing for /", "Python http.server"),
|
|
85
|
+
(r"<title>Directory: /", "Caddy file_server browse"),
|
|
86
|
+
(r"<table[^>]*class=['\"]listing", "Caddy file_server browse"),
|
|
87
|
+
(r"<pre><a href=['\"]\.\.['\"]>\.\./</a>", "nginx default autoindex"),
|
|
88
|
+
(r"<head>\s*<title>Index of [^<]+</title>", "Generic autoindex"),
|
|
89
|
+
(r"^\s*<\?xml.+<ListBucketResult", "AWS S3 ListBucket XML"),
|
|
90
|
+
(r"<EnumerationResults", "Azure Blob list-blob XML"),
|
|
91
|
+
(r"<title>Objects:", "GCS bucket browse"),
|
|
92
|
+
(r"<h1>Listing", "Rails / Rack::Directory listing"),
|
|
93
|
+
(r"<title>Index - /", "Lighttpd mod_dirlisting"),
|
|
94
|
+
(r"^\s*<!DOCTYPE\s+html>[\s\S]+<h1>\s*Index of\s+/", "Variant Index of"),
|
|
95
|
+
]
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def _is_autoindex(body_text: str) -> tuple[bool, str | None]:
|
|
99
|
+
"""Returns (matched, framework_name) — True if body looks like an autoindex page."""
|
|
100
|
+
sample = (body_text or "")[:4096]
|
|
101
|
+
for pattern, framework in AUTOINDEX_PATTERNS:
|
|
102
|
+
if re.search(pattern, sample, re.MULTILINE | re.IGNORECASE):
|
|
103
|
+
return True, framework
|
|
104
|
+
return False, None
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def main(argv: list[str] | None = None) -> int:
|
|
108
|
+
parser = argparse.ArgumentParser(description="Directory-listing probe")
|
|
109
|
+
parser.add_argument("url")
|
|
110
|
+
parser.add_argument("--authorized", action="store_true")
|
|
111
|
+
parser.add_argument("--output", default=None)
|
|
112
|
+
parser.add_argument("--format", choices=("json", "jsonl", "markdown"), default="markdown")
|
|
113
|
+
parser.add_argument("--min-severity", choices=("critical", "high", "medium", "low", "info"), default="info")
|
|
114
|
+
parser.add_argument("--timeout", type=float, default=10.0)
|
|
115
|
+
parser.add_argument("--paths-file", default=None, help="Custom probe set (one path per line); replaces default")
|
|
116
|
+
args = parser.parse_args(argv)
|
|
117
|
+
|
|
118
|
+
require_authorization(args.url, args.authorized)
|
|
119
|
+
|
|
120
|
+
sess = make_session(timeout=args.timeout)
|
|
121
|
+
base = args.url.rstrip("/") + "/"
|
|
122
|
+
findings: list[Finding] = []
|
|
123
|
+
|
|
124
|
+
probe_set = PROBES
|
|
125
|
+
if args.paths_file:
|
|
126
|
+
paths = Path(args.paths_file).read_text().splitlines()
|
|
127
|
+
probe_set = [(p.strip().rstrip("/") + "/", Severity.MEDIUM, "custom") for p in paths if p.strip()]
|
|
128
|
+
|
|
129
|
+
for path, sev, control in probe_set:
|
|
130
|
+
url = base + path.lstrip("/")
|
|
131
|
+
resp = safe_get(sess, url, timeout=args.timeout, allow_redirects=False)
|
|
132
|
+
if resp is None or resp.status_code != 200:
|
|
133
|
+
continue
|
|
134
|
+
body = resp.text or ""
|
|
135
|
+
ctype = resp.headers.get("Content-Type", "")
|
|
136
|
+
# Reject application/* responses (e.g. JSON APIs returning a 200) —
|
|
137
|
+
# those aren't autoindex pages
|
|
138
|
+
if "html" not in ctype.lower() and "xml" not in ctype.lower():
|
|
139
|
+
continue
|
|
140
|
+
matched, framework = _is_autoindex(body)
|
|
141
|
+
if not matched:
|
|
142
|
+
continue
|
|
143
|
+
findings.append(
|
|
144
|
+
Finding(
|
|
145
|
+
skill_id=SKILL_ID,
|
|
146
|
+
title=f"Directory listing exposed at /{path} ({framework})",
|
|
147
|
+
severity=sev,
|
|
148
|
+
target=url,
|
|
149
|
+
detail=(
|
|
150
|
+
f"GET {url} returned 200 with HTML body matching the "
|
|
151
|
+
f"{framework} autoindex fingerprint. Every file in this "
|
|
152
|
+
"directory is enumerable to any external requestor, "
|
|
153
|
+
"including files the application never explicitly linked to."
|
|
154
|
+
),
|
|
155
|
+
remediation=(
|
|
156
|
+
f"Disable autoindex for {path!r} at the web-server layer. "
|
|
157
|
+
"See references/PLAYBOOK.md for per-server snippets "
|
|
158
|
+
"(nginx `autoindex off`, Apache `Options -Indexes`, "
|
|
159
|
+
"Caddy drop the `file_server browse` directive, S3 "
|
|
160
|
+
"remove `s3:ListBucket` from the public bucket policy)."
|
|
161
|
+
),
|
|
162
|
+
cwe_id="CWE-548",
|
|
163
|
+
affected_control=control,
|
|
164
|
+
evidence=(
|
|
165
|
+
("framework", framework or "unknown"),
|
|
166
|
+
("content_type", ctype),
|
|
167
|
+
("body_sample_len", len(body)),
|
|
168
|
+
),
|
|
169
|
+
)
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
floor = Severity(args.min_severity)
|
|
173
|
+
findings = [f for f in findings if f.severity.numeric >= floor.numeric]
|
|
174
|
+
|
|
175
|
+
emit(findings, args.output, args.format, args.url)
|
|
176
|
+
return exit_code(findings)
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
if __name__ == "__main__":
|
|
180
|
+
sys.exit(main())
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: detecting-eval-exec-usage
|
|
3
|
+
description: |
|
|
4
|
+
Scan a source tree for dynamic-code-execution APIs that an attacker
|
|
5
|
+
can hijack: Python eval / exec / compile, JavaScript eval /
|
|
6
|
+
Function() / setTimeout(string), Ruby eval / instance_eval /
|
|
7
|
+
class_eval, Java ScriptEngine, PHP eval / assert($str), .NET
|
|
8
|
+
Activator.CreateInstance / Reflection.Emit with dynamic input.
|
|
9
|
+
Use when: pre-commit gate on any application that parses
|
|
10
|
+
user-uploaded code (rule engines, formula evaluators,
|
|
11
|
+
plugin systems), or post-bug-report when "we run user-supplied
|
|
12
|
+
expressions."
|
|
13
|
+
Threshold: any call to eval / exec / Function / similar where the
|
|
14
|
+
argument is not a string literal.
|
|
15
|
+
Trigger with: "scan eval", "find dynamic exec", "audit eval calls",
|
|
16
|
+
"code injection patterns".
|
|
17
|
+
allowed-tools:
|
|
18
|
+
- Read
|
|
19
|
+
- Bash(python3:*)
|
|
20
|
+
- Glob
|
|
21
|
+
- Grep
|
|
22
|
+
disallowed-tools:
|
|
23
|
+
- Bash(rm:*)
|
|
24
|
+
- Bash(curl:*)
|
|
25
|
+
version: 3.0.0-dev
|
|
26
|
+
author: Jeremy Longshore <jeremy@intentsolutions.io>
|
|
27
|
+
license: MIT
|
|
28
|
+
compatibility: Designed for Claude Code
|
|
29
|
+
tags:
|
|
30
|
+
- security
|
|
31
|
+
- static-analysis
|
|
32
|
+
- code-injection
|
|
33
|
+
- pentest
|
|
34
|
+
---
|
|
35
|
+
|
|
36
|
+
# Detecting eval / exec Usage
|
|
37
|
+
|
|
38
|
+
## Overview
|
|
39
|
+
|
|
40
|
+
Dynamic-code-execution APIs (CWE-95 Eval Injection) let an
|
|
41
|
+
application interpret a string as code at runtime. If the string
|
|
42
|
+
contains anything user-controllable, the application has handed
|
|
43
|
+
the attacker arbitrary code execution.
|
|
44
|
+
|
|
45
|
+
The defensive posture: don't use these APIs. The exceptions are
|
|
46
|
+
narrow: rule engines, formula evaluators (spreadsheet `=` formulas),
|
|
47
|
+
plugin systems with explicit sandboxing. For everything else,
|
|
48
|
+
there's almost always a safer alternative.
|
|
49
|
+
|
|
50
|
+
## When the skill produces findings
|
|
51
|
+
|
|
52
|
+
| Finding | Severity | Threshold | Affected control |
|
|
53
|
+
|---|---|---|---|
|
|
54
|
+
| Python `eval(...)` with non-literal | **CRITICAL** | argument contains var ref | CWE-95 |
|
|
55
|
+
| Python `exec(...)` with non-literal | **CRITICAL** | argument contains var ref | CWE-95 |
|
|
56
|
+
| Python `compile(...)` with non-literal | **HIGH** | source string contains var | CWE-95 |
|
|
57
|
+
| Python `__import__(var)` | **HIGH** | dynamic module loading | CWE-95 |
|
|
58
|
+
| JS `eval(...)` | **CRITICAL** | any | CWE-95 |
|
|
59
|
+
| JS `new Function(str)` | **CRITICAL** | any non-literal | CWE-95 |
|
|
60
|
+
| JS `setTimeout/setInterval(string)` | **HIGH** | string instead of function | CWE-95 |
|
|
61
|
+
| Ruby `eval(...)`/`instance_eval(...)`/`class_eval(...)` | **CRITICAL** | non-literal | CWE-95 |
|
|
62
|
+
| PHP `eval(...)` | **CRITICAL** | always | CWE-95 |
|
|
63
|
+
| PHP `assert($str)` | **CRITICAL** | (legacy code-eval form) | CWE-95 |
|
|
64
|
+
| PHP `create_function` | **CRITICAL** | deprecated, eval-equivalent | CWE-95 |
|
|
65
|
+
| Java `ScriptEngineManager` + eval | **HIGH** | dynamic script execution | CWE-95 |
|
|
66
|
+
| C# `Activator.CreateInstance(Type.GetType(str))` | **HIGH** | type loading from string | CWE-95 |
|
|
67
|
+
|
|
68
|
+
## Prerequisites
|
|
69
|
+
|
|
70
|
+
- Python 3.9+
|
|
71
|
+
- Source tree on local filesystem
|
|
72
|
+
|
|
73
|
+
## Instructions
|
|
74
|
+
|
|
75
|
+
### Run
|
|
76
|
+
|
|
77
|
+
```bash
|
|
78
|
+
python3 ${CLAUDE_PLUGIN_ROOT}/skills/detecting-eval-exec-usage/scripts/scan_eval.py /path/to/repo
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
Options: `--output FILE`, `--format json|jsonl|markdown`,
|
|
82
|
+
`--min-severity`, `--include-tests`, `--languages LIST`.
|
|
83
|
+
|
|
84
|
+
### Interpret
|
|
85
|
+
|
|
86
|
+
CRITICAL = direct RCE vector. Replace the dynamic execution with
|
|
87
|
+
explicit logic (lookup table, switch statement) or a sandboxed
|
|
88
|
+
expression library (Python `simpleeval`, JavaScript `expr-eval`,
|
|
89
|
+
Ruby `Dentaku`).
|
|
90
|
+
|
|
91
|
+
### Remediation
|
|
92
|
+
|
|
93
|
+
See `references/PLAYBOOK.md`.
|
|
94
|
+
|
|
95
|
+
## Examples
|
|
96
|
+
|
|
97
|
+
### Pre-commit
|
|
98
|
+
|
|
99
|
+
```bash
|
|
100
|
+
python3 ${CLAUDE_PLUGIN_ROOT}/skills/detecting-eval-exec-usage/scripts/scan_eval.py \
|
|
101
|
+
--min-severity high $(git diff --name-only main...HEAD | tr '\n' ' ')
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
### CI
|
|
105
|
+
|
|
106
|
+
```yaml
|
|
107
|
+
- run: |
|
|
108
|
+
python3 plugins/security/penetration-tester/skills/detecting-eval-exec-usage/scripts/scan_eval.py \
|
|
109
|
+
. --min-severity high
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
## Output
|
|
113
|
+
|
|
114
|
+
JSON / JSONL / Markdown. Exit codes: 0 / 1 / 2.
|
|
115
|
+
|
|
116
|
+
## Error Handling
|
|
117
|
+
|
|
118
|
+
False positive on `eval("'literal'")` — the value is a constant
|
|
119
|
+
string. Verify the regex match by reading the source line.
|
|
120
|
+
|
|
121
|
+
## Resources
|
|
122
|
+
|
|
123
|
+
- `references/THEORY.md` — Why dynamic-code execution is the
|
|
124
|
+
highest-impact injection class, sandbox limits, the
|
|
125
|
+
formula-evaluator design pattern
|
|
126
|
+
- `references/PLAYBOOK.md` — Per-language safe alternatives
|
|
127
|
+
(Python simpleeval / ast.literal_eval, JS expression-eval
|
|
128
|
+
libraries, Ruby Dentaku, Java scripting sandboxes)
|
|
@@ -0,0 +1,306 @@
|
|
|
1
|
+
# Eval / Exec Remediation Playbook
|
|
2
|
+
|
|
3
|
+
The universal answer: replace dynamic-code execution with explicit
|
|
4
|
+
logic OR a sandboxed expression library. Per-language patterns
|
|
5
|
+
below.
|
|
6
|
+
|
|
7
|
+
## Python
|
|
8
|
+
|
|
9
|
+
### Before
|
|
10
|
+
|
|
11
|
+
```python
|
|
12
|
+
result = eval(user_expression)
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
### After (sandboxed expression eval)
|
|
16
|
+
|
|
17
|
+
```python
|
|
18
|
+
from simpleeval import simple_eval
|
|
19
|
+
result = simple_eval(user_expression, names={"x": 10, "y": 20})
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
### After (literal-only eval)
|
|
23
|
+
|
|
24
|
+
```python
|
|
25
|
+
import ast
|
|
26
|
+
# Only safe for literal values, NOT expressions
|
|
27
|
+
parsed = ast.literal_eval(user_input) # raises on anything non-literal
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
### After (lookup table for choice)
|
|
31
|
+
|
|
32
|
+
```python
|
|
33
|
+
# Before: eval("function_" + name + "()")
|
|
34
|
+
# After: explicit dispatch
|
|
35
|
+
HANDLERS = {
|
|
36
|
+
"process_a": process_a,
|
|
37
|
+
"process_b": process_b,
|
|
38
|
+
}
|
|
39
|
+
handler = HANDLERS.get(name)
|
|
40
|
+
if handler is None:
|
|
41
|
+
raise ValueError(f"Unknown handler: {name}")
|
|
42
|
+
result = handler()
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
### Dynamic class instantiation
|
|
46
|
+
|
|
47
|
+
```python
|
|
48
|
+
# Before: cls = eval(class_name); obj = cls()
|
|
49
|
+
# After:
|
|
50
|
+
ALLOWED_CLASSES = {
|
|
51
|
+
"TypeA": TypeA,
|
|
52
|
+
"TypeB": TypeB,
|
|
53
|
+
}
|
|
54
|
+
cls = ALLOWED_CLASSES[class_name]
|
|
55
|
+
obj = cls()
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
## JavaScript
|
|
59
|
+
|
|
60
|
+
### Before
|
|
61
|
+
|
|
62
|
+
```javascript
|
|
63
|
+
const result = eval(userInput);
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
### After (expression library)
|
|
67
|
+
|
|
68
|
+
```javascript
|
|
69
|
+
const { Parser } = require('expr-eval');
|
|
70
|
+
const result = Parser.evaluate(userInput, { x: 10, y: 20 });
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
### After (Function constructor → don't)
|
|
74
|
+
|
|
75
|
+
```javascript
|
|
76
|
+
// Before:
|
|
77
|
+
const fn = new Function('x', userBody);
|
|
78
|
+
|
|
79
|
+
// After: parse the function shape declaratively, never construct from string
|
|
80
|
+
const HANDLERS = {
|
|
81
|
+
'double': (x) => x * 2,
|
|
82
|
+
'square': (x) => x * x,
|
|
83
|
+
};
|
|
84
|
+
const fn = HANDLERS[userInput];
|
|
85
|
+
if (!fn) throw new Error('Unknown handler');
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
### setTimeout / setInterval — use function reference
|
|
89
|
+
|
|
90
|
+
```javascript
|
|
91
|
+
// Before
|
|
92
|
+
setTimeout("doThing()", 1000);
|
|
93
|
+
|
|
94
|
+
// After
|
|
95
|
+
setTimeout(doThing, 1000);
|
|
96
|
+
// or
|
|
97
|
+
setTimeout(() => doThing(arg), 1000);
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
### JSON parse instead of eval
|
|
101
|
+
|
|
102
|
+
```javascript
|
|
103
|
+
// Before
|
|
104
|
+
const data = eval('(' + jsonString + ')');
|
|
105
|
+
|
|
106
|
+
// After
|
|
107
|
+
const data = JSON.parse(jsonString);
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
## Ruby
|
|
111
|
+
|
|
112
|
+
### Before
|
|
113
|
+
|
|
114
|
+
```ruby
|
|
115
|
+
result = eval(user_expression)
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
### After (Dentaku for expressions)
|
|
119
|
+
|
|
120
|
+
```ruby
|
|
121
|
+
require 'dentaku'
|
|
122
|
+
calc = Dentaku::Calculator.new
|
|
123
|
+
result = calc.evaluate(user_expression, x: 10, y: 20)
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
### After (avoid instance_eval / class_eval on user strings)
|
|
127
|
+
|
|
128
|
+
```ruby
|
|
129
|
+
# Before
|
|
130
|
+
obj.instance_eval(user_code)
|
|
131
|
+
|
|
132
|
+
# After: define a narrow DSL, evaluate via method dispatch
|
|
133
|
+
ALLOWED_OPS = {
|
|
134
|
+
'increment' => :increment,
|
|
135
|
+
'reset' => :reset,
|
|
136
|
+
}
|
|
137
|
+
op = ALLOWED_OPS[user_input]
|
|
138
|
+
raise 'Unknown op' unless op
|
|
139
|
+
obj.send(op)
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
## PHP
|
|
143
|
+
|
|
144
|
+
### Before
|
|
145
|
+
|
|
146
|
+
```php
|
|
147
|
+
$result = eval($code);
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
### After: just don't
|
|
151
|
+
|
|
152
|
+
PHP's eval is uniquely dangerous because it injects into the
|
|
153
|
+
current scope. There's no sandboxed-eval alternative in the
|
|
154
|
+
standard library. Replace with explicit logic / dispatch table.
|
|
155
|
+
|
|
156
|
+
```php
|
|
157
|
+
$handlers = [
|
|
158
|
+
'process_a' => 'process_a',
|
|
159
|
+
'process_b' => 'process_b',
|
|
160
|
+
];
|
|
161
|
+
if (!isset($handlers[$name])) {
|
|
162
|
+
throw new InvalidArgumentException("Unknown handler: $name");
|
|
163
|
+
}
|
|
164
|
+
$fn = $handlers[$name];
|
|
165
|
+
$result = $fn();
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
### assert as eval (legacy)
|
|
169
|
+
|
|
170
|
+
```php
|
|
171
|
+
// Before — yes really, this used to work as eval
|
|
172
|
+
assert($userString);
|
|
173
|
+
|
|
174
|
+
// After
|
|
175
|
+
// Remove. assert() now is a real assertion in PHP 7+, but old
|
|
176
|
+
// code that relied on the eval-form should be replaced with
|
|
177
|
+
// explicit dispatch as above.
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
### create_function — deprecated
|
|
181
|
+
|
|
182
|
+
```php
|
|
183
|
+
// Before (deprecated since PHP 7.2, removed PHP 8.0)
|
|
184
|
+
$fn = create_function('$x', $userBody);
|
|
185
|
+
|
|
186
|
+
// After: anonymous functions / closures with explicit body
|
|
187
|
+
$multiplier = function ($x) use ($factor) {
|
|
188
|
+
return $x * $factor;
|
|
189
|
+
};
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
## Java — sandboxed scripting
|
|
193
|
+
|
|
194
|
+
### Before (Nashorn / GraalJS with full access)
|
|
195
|
+
|
|
196
|
+
```java
|
|
197
|
+
ScriptEngine engine = new ScriptEngineManager().getEngineByName("JavaScript");
|
|
198
|
+
Object result = engine.eval(userScript);
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
### After (GraalJS with restricted permissions)
|
|
202
|
+
|
|
203
|
+
```java
|
|
204
|
+
import org.graalvm.polyglot.*;
|
|
205
|
+
try (Context cx = Context.newBuilder("js")
|
|
206
|
+
.allowHostAccess(HostAccess.NONE)
|
|
207
|
+
.allowHostClassLookup(name -> false)
|
|
208
|
+
.allowIO(false)
|
|
209
|
+
.allowCreateProcess(false)
|
|
210
|
+
.allowCreateThread(false)
|
|
211
|
+
.build()) {
|
|
212
|
+
Value result = cx.eval("js", userScript);
|
|
213
|
+
}
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
### Or: don't use scripting at all
|
|
217
|
+
|
|
218
|
+
For most use cases where Java code shells out to a script engine,
|
|
219
|
+
the right answer is to define a domain-specific configuration
|
|
220
|
+
format (JSON / YAML) parsed by your Java code, with the
|
|
221
|
+
operations dispatched via a sealed-class hierarchy.
|
|
222
|
+
|
|
223
|
+
## C# / .NET
|
|
224
|
+
|
|
225
|
+
### Avoid Type.GetType(str) for dynamic class loading
|
|
226
|
+
|
|
227
|
+
### Before
|
|
228
|
+
|
|
229
|
+
```csharp
|
|
230
|
+
Type t = Type.GetType(userTypeName);
|
|
231
|
+
object instance = Activator.CreateInstance(t);
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
### After
|
|
235
|
+
|
|
236
|
+
```csharp
|
|
237
|
+
// Allow-list of permitted types
|
|
238
|
+
static readonly IReadOnlyDictionary<string, Type> ALLOWED_TYPES =
|
|
239
|
+
new Dictionary<string, Type> {
|
|
240
|
+
{ "TypeA", typeof(TypeA) },
|
|
241
|
+
{ "TypeB", typeof(TypeB) },
|
|
242
|
+
};
|
|
243
|
+
|
|
244
|
+
if (!ALLOWED_TYPES.TryGetValue(userTypeName, out Type t)) {
|
|
245
|
+
throw new ArgumentException($"Unknown type: {userTypeName}");
|
|
246
|
+
}
|
|
247
|
+
object instance = Activator.CreateInstance(t);
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
## Plugin system patterns (safe)
|
|
251
|
+
|
|
252
|
+
If you genuinely need to run user-supplied logic:
|
|
253
|
+
|
|
254
|
+
### Pattern 1 — WASM plugins
|
|
255
|
+
|
|
256
|
+
```rust
|
|
257
|
+
// Host runtime (Rust + Wasmer)
|
|
258
|
+
use wasmer::{Store, Module, Instance, imports};
|
|
259
|
+
let module = Module::new(&store, plugin_wasm_bytes)?;
|
|
260
|
+
let instance = Instance::new(&store, &module, &imports! {})?;
|
|
261
|
+
// Call exported functions; no system access by default
|
|
262
|
+
let result = instance.exports.get_function("process")?.call(&[input.into()])?;
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
### Pattern 2 — V8 isolate (Node.js)
|
|
266
|
+
|
|
267
|
+
```javascript
|
|
268
|
+
const vm = require('vm');
|
|
269
|
+
const context = vm.createContext({ /* explicit allow-list of globals */ });
|
|
270
|
+
const result = vm.runInContext(userCode, context, {
|
|
271
|
+
timeout: 1000, // hard timeout
|
|
272
|
+
breakOnSigint: true,
|
|
273
|
+
});
|
|
274
|
+
```
|
|
275
|
+
|
|
276
|
+
Note: Node's `vm` module is NOT a true sandbox — there are escape
|
|
277
|
+
techniques. For true isolation, use a separate process or
|
|
278
|
+
`isolated-vm` library.
|
|
279
|
+
|
|
280
|
+
### Pattern 3 — Containerized worker
|
|
281
|
+
|
|
282
|
+
Spawn a Docker container with the user's code, read-only
|
|
283
|
+
filesystem, no network, memory + CPU limits, timeout. The
|
|
284
|
+
boundary is the container runtime, not the application process.
|
|
285
|
+
|
|
286
|
+
## Pre-commit / CI
|
|
287
|
+
|
|
288
|
+
Same pattern as previous skills:
|
|
289
|
+
|
|
290
|
+
```yaml
|
|
291
|
+
- name: eval/exec scan
|
|
292
|
+
run: |
|
|
293
|
+
python3 plugins/security/penetration-tester/skills/detecting-eval-exec-usage/scripts/scan_eval.py \
|
|
294
|
+
. --min-severity high
|
|
295
|
+
```
|
|
296
|
+
|
|
297
|
+
## Verification after remediation
|
|
298
|
+
|
|
299
|
+
```bash
|
|
300
|
+
python3 ${CLAUDE_PLUGIN_ROOT}/skills/detecting-eval-exec-usage/scripts/scan_eval.py \
|
|
301
|
+
/path/to/repo --min-severity medium
|
|
302
|
+
```
|
|
303
|
+
|
|
304
|
+
Expected: exit 0, zero MEDIUM-or-higher findings. Remaining LOW
|
|
305
|
+
findings (legitimate `ast.literal_eval` calls, GraalJS sandboxed
|
|
306
|
+
eval) are acceptable.
|