atris 3.78.0 → 3.80.0

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.
@@ -0,0 +1,300 @@
1
+ 'use strict';
2
+
3
+ // Attack classes for the sentinel audit. Each class names the trust boundary
4
+ // it guards, the source lines that make a file worth a hunter's time, and the
5
+ // guidance a hunter gets verbatim. Detection only seeds coverage: a hit is a
6
+ // place to look, never a finding. The method follows
7
+ // cloudflare/security-audit-skill (MIT): name the lower-trust principal, the
8
+ // control that should stop them, and the concrete result across the boundary.
9
+
10
+ const CODE_EXTS = new Set(['.js', '.mjs', '.cjs', '.ts', '.tsx', '.jsx', '.py', '.rb', '.go', '.java', '.php', '.rs', '.sh', '.bash', '.zsh']);
11
+ const WEB_EXTS = new Set(['.js', '.mjs', '.cjs', '.ts', '.tsx', '.jsx', '.html', '.vue', '.svelte']);
12
+ const WORKFLOW_RE = /(?:^|\/)\.github\/workflows\/[^/]+\.ya?ml$/;
13
+
14
+ const CLASSES = [
15
+ {
16
+ id: 'command-injection',
17
+ title: 'Command injection',
18
+ boundary: 'shell and process launch',
19
+ weight: 10,
20
+ exts: CODE_EXTS,
21
+ detect: [
22
+ /\b(?:exec|execSync|spawn|spawnSync|execFile|execFileSync|fork)\s*\(/,
23
+ /\bsubprocess\.(?:run|call|Popen|check_output|check_call)\s*\(/,
24
+ /\bos\.(?:system|popen)\s*\(/,
25
+ /\bshell\s*:\s*true\b/,
26
+ ],
27
+ guide: [
28
+ 'Invariant: a lower-trust value never becomes shell syntax, a program name, or an option the caller did not intend.',
29
+ 'Trace each launched command back to its arguments. A string handed to a shell (exec, execSync, shell: true, os.system) is the risky shape; an argv array is safer but still fails when the value can start with "-" and become a flag, or picks the binary itself.',
30
+ 'Check who controls the value: a remote request, a file in an untrusted repo, a task title, model output, an environment variable set by someone else. A value the same local user typed is self-impact, not a finding.',
31
+ 'Look for quoting done by hand, values joined into one string, and option injection through "--" missing before a user path.',
32
+ ],
33
+ },
34
+ {
35
+ id: 'code-injection',
36
+ title: 'Code injection',
37
+ boundary: 'code evaluation',
38
+ weight: 10,
39
+ exts: CODE_EXTS,
40
+ detect: [
41
+ /\beval\s*\(/,
42
+ /\bnew\s+Function\s*\(/,
43
+ /\bvm\.(?:runIn\w+|Script|compileFunction)\b/,
44
+ /\brequire\s*\(\s*(?!['"`])[^)]/,
45
+ /\bimport\s*\(\s*(?!['"`])[^)]/,
46
+ /\b(?:exec|compile)\s*\(\s*(?!['"])/,
47
+ ],
48
+ guide: [
49
+ 'Invariant: text from a lower-trust source is never run as code, and a module path is never chosen by it.',
50
+ 'Follow eval, new Function, vm, and dynamic require or import back to their input. A dynamic require of a path built from a file in a cloned repo, a plugin folder, or a downloaded pack loads attacker code.',
51
+ 'A require of a path the package itself ships is intended. Name the exact writer of the loaded file before calling it a finding.',
52
+ ],
53
+ },
54
+ {
55
+ id: 'path-traversal',
56
+ title: 'Path traversal and file write',
57
+ boundary: 'filesystem path',
58
+ weight: 8,
59
+ exts: CODE_EXTS,
60
+ detect: [
61
+ /\b(?:readFileSync|writeFileSync|readFile|writeFile|appendFileSync|createReadStream|createWriteStream|unlinkSync|rmSync|rmdirSync|renameSync|copyFileSync|symlinkSync|cpSync)\s*\(\s*(?!['"`])/,
62
+ /\bpath\.(?:join|resolve)\s*\([^)]*\b(?:req|params|query|body|name|slug|id|input|args?|file|target)\b/,
63
+ /\bextract(?:All)?\s*\(|\btar\b.*\bx\b|\bunzip\b/,
64
+ ],
65
+ guide: [
66
+ 'Invariant: a path built from a lower-trust value stays inside the directory the code means, after normalization and symlink resolution.',
67
+ 'Check every join of a name, slug, id, archive entry, or url segment into a path. "..", absolute paths, encoded separators, and symlinks already in the folder are the escapes.',
68
+ 'A prefix check with startsWith on an unresolved path, or without a trailing separator, is a common miss. Writing through a symlink an attacker planted is the same class.',
69
+ 'A path the local user chose on their own command line is not a boundary crossing.',
70
+ ],
71
+ },
72
+ {
73
+ id: 'outbound-request',
74
+ title: 'Outbound requests and SSRF',
75
+ boundary: 'outbound network request',
76
+ weight: 7,
77
+ exts: CODE_EXTS,
78
+ detect: [
79
+ /\bfetch\s*\(\s*(?!['"`]https?:\/\/)/,
80
+ /\bhttps?\.(?:request|get)\s*\(/,
81
+ /\baxios(?:\.\w+)?\s*\(/,
82
+ /\brequests\.(?:get|post|put|patch|delete|request)\s*\(/,
83
+ /\burllib\.request\.urlopen\s*\(/,
84
+ ],
85
+ guide: [
86
+ 'Invariant: a lower-trust value cannot pick the host, scheme, or port of a request made with this process\'s network position or credentials.',
87
+ 'Check whether a url, redirect target, webhook, or api base can come from a request, a config file in an untrusted repo, or model output. Credentials attached to a request whose host the caller chose leak those credentials.',
88
+ 'Look at redirect following and at allowlists that check the string before parsing it.',
89
+ ],
90
+ },
91
+ {
92
+ id: 'access-control',
93
+ title: 'Access control on entry points',
94
+ boundary: 'authentication and authorization',
95
+ weight: 9,
96
+ exts: CODE_EXTS,
97
+ detect: [
98
+ /\b(?:app|router|server|api)\.(?:get|post|put|patch|delete|all|use|route)\s*\(\s*['"`]\//,
99
+ /@(?:app|router|bp|blueprint)\.(?:get|post|put|patch|delete|route)\s*\(/,
100
+ /\bcreateServer\s*\(/,
101
+ /\.listen\s*\(\s*(?:\d|port|PORT)/,
102
+ /\b(?:req|request)\.(?:headers|query|params|body)\b/,
103
+ ],
104
+ guide: [
105
+ 'Invariant: every action that reads or changes another principal\'s data, or spends shared resources, checks who is asking and whether they may.',
106
+ 'List the routes or handlers in the assigned paths. For each, find the identity check and the ownership check. Compare sibling routes: a check present on read but missing on update, delete, batch, or export is the classic gap.',
107
+ 'For a local server, check the bind address, the origin and host checks, and whether a web page in the user\'s browser can reach it (DNS rebinding, missing token, permissive CORS).',
108
+ 'Error responses that differ between "not yours" and "not found" are an enumeration oracle only when ids are secret.',
109
+ ],
110
+ },
111
+ {
112
+ id: 'secrets-handling',
113
+ title: 'Secret handling and exposure',
114
+ boundary: 'credential custody',
115
+ weight: 9,
116
+ exts: CODE_EXTS,
117
+ detect: [
118
+ /process\.env\.[A-Z0-9_]*(?:KEY|TOKEN|SECRET|PASSWORD|CREDENTIAL)/,
119
+ /os\.environ(?:\.get)?\s*[[(]\s*['"][A-Z0-9_]*(?:KEY|TOKEN|SECRET|PASSWORD)/,
120
+ /\b(?:authorization|x-api-key|api[_-]?key|access[_-]?token|refresh[_-]?token)\b/i,
121
+ /\bcredentials?\.json\b|\bkeychain\b|\.netrc\b/i,
122
+ ],
123
+ guide: [
124
+ 'Invariant: a credential reaches only the party it was issued for, and never lands in logs, errors, receipts, prompts, child process environments, or files others can read.',
125
+ 'Trace each token from where it is read to every place it is written or sent. Check file modes on saved credentials, environment passed to child processes and engines, and error paths that print a request.',
126
+ 'A token sent to a host the caller can choose is also an outbound-request finding; name both classes in the root cause.',
127
+ ],
128
+ },
129
+ {
130
+ id: 'agent-and-llm',
131
+ title: 'Prompt injection and agent tools',
132
+ boundary: 'model output to tool action',
133
+ weight: 9,
134
+ exts: CODE_EXTS,
135
+ detect: [
136
+ /--dangerously-skip-permissions|--yolo\b|bypassPermissions|danger-full-access|--permission-mode['"`,\s]+(?:auto|bypass)/,
137
+ /\b(?:anthropic|openai|messages\.create|chat\.completions|responses\.create)\b/i,
138
+ /\b(?:prompt|system_prompt|systemPrompt)\s*[:=+]/,
139
+ /\btool_use\b|\btools\s*:\s*\[/,
140
+ ],
141
+ guide: [
142
+ 'Invariant: text a model reads from a lower-trust source (email, web page, issue, file in a cloned repo, another agent) cannot make the agent take an action the operator did not allow.',
143
+ 'Find where untrusted text enters a prompt, then what the model can do next: run commands, write files, send messages, spend money, push code. Permission bypass flags turn a prompt injection into code execution.',
144
+ 'Check that model output is treated as data where it reaches a shell, a path, a url, a sql query, or html. Check that secrets are not put in prompts that leave the machine.',
145
+ 'A model doing what its own operator asked is intended. The finding needs a lower-trust author of the text.',
146
+ ],
147
+ },
148
+ {
149
+ id: 'sql-injection',
150
+ title: 'Query injection',
151
+ boundary: 'database query',
152
+ weight: 8,
153
+ exts: CODE_EXTS,
154
+ detect: [
155
+ /\b(?:SELECT|INSERT|UPDATE|DELETE)\b[^;\n]*(?:\$\{|['"`]\s*\+|%s|\.format\(|\bf["'])/i,
156
+ /\.(?:query|execute|raw|exec)\s*\(\s*(?:`[^`]*\$\{|f["']|['"][^'"]*['"]\s*\+)/,
157
+ /\$where\b|\.find\s*\(\s*(?:req|request)\./,
158
+ ],
159
+ guide: [
160
+ 'Invariant: lower-trust values reach a query only as bound parameters, never as query text, identifiers, or operators.',
161
+ 'Trace the values in each built query. Table and column names chosen by input, ORDER BY from a request, and json operators in document stores are the misses that parameter binding does not cover.',
162
+ 'Check tenant scoping in the same query: a missing owner filter is an access-control finding in this file.',
163
+ ],
164
+ },
165
+ {
166
+ id: 'client-side-injection',
167
+ title: 'Client-side injection',
168
+ boundary: 'browser rendering',
169
+ weight: 7,
170
+ exts: WEB_EXTS,
171
+ detect: [
172
+ /\.innerHTML\s*[+]?=/,
173
+ /\bdangerouslySetInnerHTML\b/,
174
+ /\bdocument\.write\s*\(/,
175
+ /\bv-html\b|\{@html\b/,
176
+ /\bpostMessage\s*\(|addEventListener\s*\(\s*['"]message['"]/,
177
+ /\bhref\s*=\s*\{?\s*(?!['"](?:https?:|\/|#))/,
178
+ ],
179
+ guide: [
180
+ 'Invariant: text from another user or an outside source renders as text, never as markup or script, and cross-window messages are accepted only from expected origins.',
181
+ 'Trace each html sink back to its value. Markdown renderers, link hrefs that allow javascript:, and message handlers that skip the origin check are the common gaps.',
182
+ 'Self-xss with no other viewer is not a finding.',
183
+ ],
184
+ },
185
+ {
186
+ id: 'unsafe-parsing',
187
+ title: 'Unsafe parsing and deserialization',
188
+ boundary: 'parser and deserializer',
189
+ weight: 7,
190
+ exts: CODE_EXTS,
191
+ detect: [
192
+ /\byaml\.load\s*\(/,
193
+ /\bpickle\.loads?\s*\(|\bmarshal\.loads?\s*\(/,
194
+ /\bunserialize\s*\(|\bMarshal\.load\b/,
195
+ /\b__proto__\b|\bconstructor\s*\[\s*['"]prototype/,
196
+ /\bObject\.assign\s*\(\s*\{\}\s*,\s*(?:req|body|input|data|JSON\.parse)/,
197
+ ],
198
+ guide: [
199
+ 'Invariant: parsing lower-trust input never runs code, never changes shared object prototypes, and never produces an object the next step trusts more than the input.',
200
+ 'Check unsafe loaders (yaml.load, pickle), deep merges of parsed json into config objects (prototype pollution), and parser differentials where two components read the same input differently.',
201
+ ],
202
+ },
203
+ {
204
+ id: 'weak-crypto',
205
+ title: 'Weak tokens and comparisons',
206
+ boundary: 'token generation and verification',
207
+ weight: 6,
208
+ exts: CODE_EXTS,
209
+ detect: [
210
+ /Math\.random\s*\(\)[^\n]*(?:token|secret|nonce|session|key|id|code|password)/i,
211
+ /(?:token|secret|nonce|session|key|code|password)[^\n]*Math\.random\s*\(\)/i,
212
+ /createHash\s*\(\s*['"](?:md5|sha1)['"]/,
213
+ /\b\w*(?:token|secret|signature|hmac|digest)\w*\s*(?:===|!==|==|!=)\s*\w*(?:token|secret|signature|hmac|digest|expected)/i,
214
+ /\bjwt\.(?:decode|verify)\s*\(|algorithms?\s*:\s*\[?\s*['"]none['"]/,
215
+ ],
216
+ guide: [
217
+ 'Invariant: secrets, session ids, and one-time codes are unguessable, and checks of them do not leak timing or accept forged values.',
218
+ 'Math.random for anything an attacker must not guess, plain === on a secret over the network, md5 or sha1 used for integrity, and jwt decode without verify are the shapes.',
219
+ 'A random id used only for display or file names is not a finding.',
220
+ ],
221
+ },
222
+ {
223
+ id: 'supply-chain',
224
+ title: 'Supply chain and release',
225
+ boundary: 'install, build, and release',
226
+ weight: 8,
227
+ exts: null,
228
+ files: (rel) => WORKFLOW_RE.test(rel) || /(?:^|\/)package\.json$/.test(rel) || /(?:^|\/)(?:Dockerfile|Makefile|install\.sh|setup\.py|pyproject\.toml)$/.test(rel),
229
+ detect: [
230
+ /"(?:preinstall|install|postinstall|prepare|prepublish)"\s*:/,
231
+ /\bpull_request_target\b|\bworkflow_run\b/,
232
+ /\buses:\s*[\w.-]+\/[\w.-]+@(?![0-9a-f]{40}\b)/,
233
+ /\bcurl\b[^|\n]*\|\s*(?:ba|z)?sh\b|\bwget\b[^|\n]*\|\s*(?:ba|z)?sh\b/,
234
+ /\$\{\{\s*github\.event\.(?:issue|pull_request|comment|head_commit|review)\b/,
235
+ /\bsecrets\.[A-Z0-9_]+/,
236
+ ],
237
+ guide: [
238
+ 'Invariant: code from a lower-trust contributor or dependency cannot run with release credentials, change what ships, or execute on a user\'s machine at install time.',
239
+ 'Check workflows triggered by forks (pull_request_target, workflow_run) that check out the fork\'s code with secrets in scope, untrusted github.event text interpolated into run: shell, actions pinned to a branch instead of a commit, and install hooks.',
240
+ 'Check curl piped to a shell, unpinned downloads without a hash, and self-update paths that do not verify what they fetch.',
241
+ ],
242
+ },
243
+ {
244
+ id: 'resource-exhaustion',
245
+ title: 'Resource exhaustion',
246
+ boundary: 'shared resource and spend',
247
+ weight: 5,
248
+ exts: CODE_EXTS,
249
+ detect: [
250
+ /\bnew\s+RegExp\s*\(\s*(?!['"`/])/,
251
+ /\bwhile\s*\(\s*true\s*\)/,
252
+ /\bmaxBuffer\s*:\s*(?:Infinity|\d{9,})/,
253
+ /\b(?:readFileSync|JSON\.parse)\s*\([^)]*\b(?:req|body|input|stdin)\b/,
254
+ ],
255
+ guide: [
256
+ 'Invariant: one lower-trust caller cannot use up memory, disk, process slots, model spend, or a shared queue that other callers depend on.',
257
+ 'Check regexes built from input, unbounded reads of request bodies or files, retry loops with no cap, and work queued per request with no limit.',
258
+ 'Never test availability live. Show the unbounded path in source and the size that a single request controls.',
259
+ ],
260
+ },
261
+ ];
262
+
263
+ const CLASS_BY_ID = new Map(CLASSES.map((c) => [c.id, c]));
264
+
265
+ // The hunting method every hunter prompt carries.
266
+ const HUNT_METHOD = [
267
+ 'Read the code at depth. Follow each input through parsing, identity, authorization, normalization, state, and the final sink. Read sibling, batch, retry, error, and legacy paths that produce the same effect.',
268
+ 'Work from a concrete invariant: name the lower-trust principal and what they start with, the value or action they control, the control that should stop them, the exact path after that control, and the smallest result across the boundary.',
269
+ 'Test sad paths only where the interface accepts them: empty, huge, duplicate, encoded, stale, concurrent, reordered, failed dependency.',
270
+ 'Stop a line of investigation as soon as the invariant is settled either way, and record the result.',
271
+ 'When you find a high or critical root cause, look for its variants inside your assigned paths. Report one candidate per root cause.',
272
+ 'You are read-only. Do not run the target, contact any network service, or use real credentials. Source evidence only.',
273
+ ];
274
+
275
+ const CANDIDATE_GATE = [
276
+ 'A candidate needs a complete repository-relative trace from where the lower-trust input enters to the sink, with real file and line numbers you read yourself.',
277
+ 'Propose "confirmed" only when the trace is complete, the impact crosses a stated boundary, every condition is listed, and no layer in source prevents it.',
278
+ 'Do not strengthen a crash into code execution, ordinary work into an outage, or a same-person action into privilege gain.',
279
+ 'If a decisive fact is outside source (deployment, proxy, provider setting, who can write a file), propose "needs_validation" with the exact missing fact. It gets no severity.',
280
+ 'A missing best practice with no affected principal or resource goes in "hardening", not in candidates. A candidate disproved by source is dropped.',
281
+ 'The fingerprint names the root cause, like "lib/foo.js:runHook/shell-from-task-title". No line numbers, no severity, no verdict in it.',
282
+ 'Return an empty candidates array when nothing survives these rules. Most units should end "covered".',
283
+ ];
284
+
285
+ const SEVERITY_ANCHORS = [
286
+ 'critical: an unauthenticated actor gains code execution, full data-store access, or takeover of arbitrary accounts.',
287
+ 'high: an actor fully defeats an explicit security control with real consequences (auth bypass, cross-tenant read or write, stored script for other users, authenticated code execution).',
288
+ 'medium: a real boundary violation with limited blast radius or uncommon preconditions.',
289
+ 'low: disclosure of non-secret internals, or an effect needing sustained effort for minimal gain.',
290
+ 'informational: confirmed but minimal impact.',
291
+ ];
292
+
293
+ module.exports = {
294
+ CLASSES,
295
+ CLASS_BY_ID,
296
+ CODE_EXTS,
297
+ HUNT_METHOD,
298
+ CANDIDATE_GATE,
299
+ SEVERITY_ANCHORS,
300
+ };