@clear-capabilities/agentic-security-scanner 0.195.6 → 0.195.8
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/CHANGELOG.md +23 -0
- package/dist/{af0905b43ac094f334a2.json → 06198b6f3fac9b1cb25f.json} +5 -3
- package/dist/agentic-security.mjs +2 -2
- package/dist/agentic-security.mjs.sha256 +1 -1
- package/package.json +5 -3
- package/src/dataflow/cross-service-taint.js +1 -1
- package/src/language/CLAUDE.md +5 -1
- package/src/language/haskell-guards.js +48 -3
- package/src/language/haskell-ir.js +2 -1
- package/src/language/haskell-models.js +7 -0
- package/src/language/haskell-parser.js +63 -1
- package/src/language/haskell-security-rules.js +83 -15
- package/src/language/haskell-trust.js +6 -2
- package/src/language/haskell-web.js +114 -11
- package/src/lineage/source-registry.js +1 -0
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
|
|
21
21
|
import { buildHaskellIR, lowerHandlerExpr, resolveName } from './haskell-ir.js';
|
|
22
22
|
import { codeOfLine } from './haskell-comments.js';
|
|
23
|
-
import { qualifyAmbiguous, HS_WEB_FRAMEWORKS, HS_WEB_MODEL_VERSION, modelStatus } from './haskell-models.js';
|
|
23
|
+
import { qualifyAmbiguous, HS_WEB_FRAMEWORKS, HS_WEB_MODEL_VERSION, modelStatus, handlerFrameworkOf } from './haskell-models.js';
|
|
24
24
|
|
|
25
25
|
export const HS_WEB_RULES_VERSION = 'haskell-web-rules/1';
|
|
26
26
|
|
|
@@ -43,6 +43,19 @@ const ADMIN_PATH = /(?:^|\/)(?:admin|internal|manage|superuser)(?:\/|$|:)/i;
|
|
|
43
43
|
const ROLE_CALL = /(?:^|\.)(?:isAdmin|hasRole|requireAdmin|checkRole|hasPermission|requireRole|requirePermission|isSuperuser)$/i;
|
|
44
44
|
const OWNER_CALL = /(?:^|\.)(?:checkOwner|ownedBy|belongsTo|isOwner|requireOwner|authorizeOwner)$/i;
|
|
45
45
|
const CSRF_CALL = /csrf|xsrf/i;
|
|
46
|
+
// Evidence that an object HAS an owner, read from the data the handler touches, never from a route or file name: a column of the statement
|
|
47
|
+
// it runs (`owner_id`, `tenant_id`, `created_by`) or a field of a record declared in the project (`invOwner`, `profileUser`). An id lookup that
|
|
48
|
+
// selects such an object without consulting the owner (the column is not in the WHERE clause; the field is never read in the handler) is a
|
|
49
|
+
// lookup the client steers, whether or not a principal was authenticated.
|
|
50
|
+
const OWNER_COL = /\b(?:owner|tenant|account|customer|author|created_by|user)_?id\b|\bowner\b/i;
|
|
51
|
+
const OWNER_FIELD = /(?:^|_)(?:owner|tenant|author|created_?by)(?:_?id)?$|(?:^|_)(?:user|account|customer)_?id$/i;
|
|
52
|
+
const SQL_VERB = /\b(?:select|update|delete|insert)\b/i;
|
|
53
|
+
// A container lookup by key (`M.lookup invoiceId m`, `HM.!? key`): not a sensitive operation (no database, file or process), but the step at which an
|
|
54
|
+
// object is selected by a client-supplied id, so it counts for the ownership judgement only.
|
|
55
|
+
const LOOKUP_CALL = /(?:^|\.)(?:lookup|!\?|!|find|findWithDefault|elemAt)$/;
|
|
56
|
+
// A call that only builds the response or lays values out: a handler whose every call is one of these does nothing a client could abuse,
|
|
57
|
+
// so a missing authentication check on it is a question about the route (its method and path), not a visible operation.
|
|
58
|
+
const RESPONSE_ONLY = /^(?:Web\.Scotty(?:\.Trans)?\.(?:text|json|html|status|setHeader|addHeader|raw|redirect|finish|raise|raiseStatus|param|params|queryParam|captureParam|header|headers|body|jsonData)|Servant(?:\.Server)?\.(?:throwError|throwAll)|Yesod(?:\.Core)?\.(?:redirect|returnJson|defaultLayout|setTitle|sendResponse|notFound|permissionDenied|invalidArgs|requireJsonBody|lookupGetParam|lookupPostParam|toWidget|hamletToRepHtml)|Prelude\.(?:pure|return|fmap|map|concatMap|unlines|lines|show|length|not|maybe|either|fst|snd|const|id|flip|unwords|words|filter|zip|lookup|elem|notElem)|Control\.Monad\.(?:when|unless|void|forM|forM_|mapM|mapM_|guard)|Control\.Monad\.IO\.Class\.liftIO|Control\.Monad\.Trans\.Class\.lift|Data\.(?:Text|Text\.Lazy|Text\.Encoding|ByteString(?:\.Char8|\.Lazy|\.Lazy\.Char8)?|Aeson|Maybe|List|Map|Map\.Strict|Char|String|Functor|Foldable)\.[^.]+)$/;
|
|
46
59
|
|
|
47
60
|
// ── AST helpers ──────────────────────────────────────────────────────────────
|
|
48
61
|
function unparen(e) { while (e && e.t === 'paren') e = e.e; return e; }
|
|
@@ -360,9 +373,14 @@ function handlerFacts(ctx, fn, opts = {}) {
|
|
|
360
373
|
for (const a of assigns) if (a.source.kind === 'call' && isGuardCall(a.source)) principals.add(a.target);
|
|
361
374
|
// ids read from the request
|
|
362
375
|
const idVars = new Set();
|
|
376
|
+
const SCOTTY_PARAM = /^Web\.Scotty(?:\.Trans)?\.(?:param|captureParam|queryParam|pathParam)$/;
|
|
377
|
+
// `docId <- param "id" :: ActionM Int` lowers to a source temporary (`$src0 <- param "id"`) and a number whose `hs.inner` names that temporary
|
|
378
|
+
const paramReads = new Map();
|
|
379
|
+
for (const a of assigns) if (a.source.kind === 'call' && SCOTTY_PARAM.test(a.source.callee)) paramReads.set(a.target, a.source);
|
|
363
380
|
for (const a of assigns) {
|
|
364
|
-
|
|
365
|
-
if (s.kind === '
|
|
381
|
+
let s = a.source.kind === 'literal' && a.source.hs && a.source.hs.inner ? a.source.hs.inner : a.source;
|
|
382
|
+
if (s.kind === 'ident' && paramReads.has(s.name)) s = paramReads.get(s.name);
|
|
383
|
+
if (s.kind === 'call' && SCOTTY_PARAM.test(s.callee)) {
|
|
366
384
|
const nm = headerArg(s);
|
|
367
385
|
if (nm && /id$/i.test(nm)) idVars.add(a.target);
|
|
368
386
|
}
|
|
@@ -388,6 +406,45 @@ function hasOwnership(ctx, facts) {
|
|
|
388
406
|
return null;
|
|
389
407
|
}
|
|
390
408
|
|
|
409
|
+
// Owner evidence for the lookups of one handler (see OWNER_COL / OWNER_FIELD). Returns the first piece found, or null.
|
|
410
|
+
function ownerEvidence(ctx, fn, facts, lookups) {
|
|
411
|
+
// 1. a statement literal handed to a lookup names an owner column outside its WHERE clause
|
|
412
|
+
for (const o of lookups) {
|
|
413
|
+
for (const a of o.call.args || []) for (const x of walkIr(a)) {
|
|
414
|
+
if (x.kind !== 'literal' || typeof x.value !== 'string' || !SQL_VERB.test(x.value)) continue;
|
|
415
|
+
const [head, ...tail] = x.value.split(/\bwhere\b/i);
|
|
416
|
+
const col = OWNER_COL.exec(head);
|
|
417
|
+
if (col && !OWNER_COL.test(tail.join(' '))) return { kind: 'owner-column-not-in-where', column: col[0], statement: x.value.slice(0, 120), line: o.line };
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
// 2. a record declared in the project has an owner field, and the handler never reads it
|
|
421
|
+
const recTypes = ctx.project && typeof ctx.project.recordTypes === 'function' ? ctx.project.recordTypes() : null;
|
|
422
|
+
if (!recTypes || !recTypes.size) return null;
|
|
423
|
+
const mentioned = new Set();
|
|
424
|
+
for (const n of Object.values(fn.cfg.nodes)) for (const root of exprsOfNode(n)) for (const e of walkIr(root)) {
|
|
425
|
+
if (e.kind === 'ident' && e.name) mentioned.add(e.name.replace(/^.*\./, ''));
|
|
426
|
+
if (e.kind === 'call' && typeof e.callee === 'string') mentioned.add(e.callee.replace(/^.*\./, ''));
|
|
427
|
+
if (e.kind === 'member' && e.prop) mentioned.add(String(e.prop));
|
|
428
|
+
}
|
|
429
|
+
for (const c of facts.conds) for (const e of walkIr(c)) { if (e.kind === 'ident' && e.name) mentioned.add(e.name.replace(/^.*\./, '')); if (e.kind === 'call' && typeof e.callee === 'string') mentioned.add(e.callee.replace(/^.*\./, '')); }
|
|
430
|
+
for (const [type, fields] of recTypes) {
|
|
431
|
+
const list = (Array.isArray(fields) ? fields : [...(fields || [])]).map((f) => (typeof f === 'string' ? f : f && f.name)).filter(Boolean);
|
|
432
|
+
const ownerField = list.find((f) => OWNER_FIELD.test(String(f).replace(/([a-z0-9])([A-Z])/g, '$1_$2')));
|
|
433
|
+
// the record must be the object the handler works with: it reads another of its fields (`invTotal inv`), or builds it. A record declared in the
|
|
434
|
+
// module that the handler never touches says nothing about what the lookup selected (a `Post` type beside a download handler).
|
|
435
|
+
const inPlay = list.some((f) => f !== ownerField && mentioned.has(f)) || mentioned.has(type);
|
|
436
|
+
if (ownerField && inPlay && !mentioned.has(ownerField)) return { kind: 'owner-field-unread', type, field: ownerField, line: lookups[0] ? lookups[0].line : fn.line };
|
|
437
|
+
}
|
|
438
|
+
return null;
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
// The first call of a handler that is neither a sensitive operation nor response plumbing: a project function, a parameter called as a function, a
|
|
442
|
+
// console write. Its presence means the handler does something the engine cannot see into.
|
|
443
|
+
function effectfulCall(facts) {
|
|
444
|
+
const o = facts.ordered.find((x) => !facts.isSensitive(x.call) && !RESPONSE_ONLY.test(x.call.callee || '') && !/^(?:>>=|>>|<\$>|<\*>|\$|\.|fmap|pure|return)$/.test(x.call.callee || ''));
|
|
445
|
+
return o ? { callee: o.call.callee, line: o.line } : null;
|
|
446
|
+
}
|
|
447
|
+
|
|
391
448
|
// ── structural order of an inline guard (WAI, Servant, Yesod) ──────────────────
|
|
392
449
|
// For these frameworks the IR reports a call in tail position (the last statement, a case alternative) at the ENCLOSING line, so a line
|
|
393
450
|
// number cannot say whether the rejection runs before the first sensitive operation. The handler's own syntax tree can: this walks it in
|
|
@@ -615,7 +672,13 @@ function analyzeRoute(ctx, route, handlerFn, opts) {
|
|
|
615
672
|
R.sensitive = facts.sensIdx >= 0;
|
|
616
673
|
// ownership / BOLA inputs
|
|
617
674
|
R.ownership = hasOwnership(ctx, facts);
|
|
618
|
-
|
|
675
|
+
// a lookup's key sits in `hs.selector` (`M.lookup invoiceId m` keeps the key beside the container), so it is judged with the arguments
|
|
676
|
+
const operands = (c) => [...(c.args || []), ...(c.hs && c.hs.selector ? [c.hs.selector] : [])];
|
|
677
|
+
const byId = (c) => operands(c).some((a) => [...identsIr(a)].some((n) => facts.idVars.has(n)));
|
|
678
|
+
R.idLookups = facts.ordered.filter((o) => facts.isSensitive(o.call) && byId(o.call));
|
|
679
|
+
R.idSelections = facts.ordered.filter((o) => !facts.isSensitive(o.call) && LOOKUP_CALL.test(o.call.callee || '') && byId(o.call));
|
|
680
|
+
R.ownerEvidence = R.idLookups.length || R.idSelections.length ? ownerEvidence(ctx, handlerFn, facts, [...R.idLookups, ...R.idSelections]) : null;
|
|
681
|
+
R.effectful = facts.sensIdx >= 0 ? null : effectfulCall(facts);
|
|
619
682
|
}
|
|
620
683
|
|
|
621
684
|
// ── Scotty ─────────────────────────────────────────────────────────────────
|
|
@@ -762,7 +825,7 @@ function expandChain(chain, acc) {
|
|
|
762
825
|
else if (name === 'ReqBody') a.order.push('body');
|
|
763
826
|
else if (name === 'BasicAuth') { a.auth = { kind: 'basic', tag: 'BasicAuth' }; a.order.push('auth'); }
|
|
764
827
|
else if (name === 'AuthProtect') { const nm = head.find((t) => t.t === 'str'); a.auth = { kind: 'token', tag: nm ? nm.v : 'AuthProtect' }; a.order.push('auth'); if (nm && /admin|role|perm/i.test(nm.v)) a.role = true; }
|
|
765
|
-
else if (name === 'Auth') { a.auth = { kind: 'token', tag: 'Auth' }; a.order.push('auth'); }
|
|
828
|
+
else if (name === 'Auth') { a.auth = { kind: 'token', tag: 'Auth' }; a.order.push('auth'); if (head.some((t) => t.t === 'con' && t.v !== 'Auth' && /admin|role|perm/i.test(t.v))) a.role = true; } // `Auth '[JWT] Admin`: the principal type names the role, as an AuthProtect tag does
|
|
766
829
|
else if (name && /^(?:Get|Post|Put|Delete|Patch)(?:NoContent)?$/.test(name)) return [finishServant(a, head)];
|
|
767
830
|
return rest.length ? expandChain(rest, a) : [finishServant(a, head)];
|
|
768
831
|
}
|
|
@@ -927,6 +990,27 @@ export function analyzeHaskellWeb(files, opts = {}) {
|
|
|
927
990
|
}
|
|
928
991
|
}
|
|
929
992
|
|
|
993
|
+
// Scotty handlers typed by the framework (`getDocument :: Connection -> ActionM ()`) that no `get`/`post` in an analysed file registers: the route
|
|
994
|
+
// table is elsewhere, so the method and path are unknown. They are analysed for object authorization only (the method decides nothing here):
|
|
995
|
+
// `UNKNOWN` is neither a write nor a privileged path, so no missing-authentication finding can come from one.
|
|
996
|
+
if (fw.has('scotty')) {
|
|
997
|
+
const registered = new Set();
|
|
998
|
+
for (const p of pending) if (p.framework === 'scotty' && p.handlerAst) { const sp = spine(p.handlerAst); const h = sp && sp.head ? sp.head : (p.handlerAst.t === 'var' ? p.handlerAst : null); if (h && h.name) registered.add(h.name); for (const n of walkAst(p.handlerAst)) if (n.t === 'var' && n.name) registered.add(n.name); }
|
|
999
|
+
for (const mod of ctx.modules) {
|
|
1000
|
+
const mods = mod.imports.map((i) => i.module);
|
|
1001
|
+
if (!mods.some((m) => /^Web\.Scotty(?:\.|$)/.test(m))) continue;
|
|
1002
|
+
for (const fun of mod.groups.funs) {
|
|
1003
|
+
const sig = mod.sigs.get(fun.name);
|
|
1004
|
+
if (!sig || registered.has(fun.name)) continue;
|
|
1005
|
+
const resultText = sig.resultText || String(sig.text || '').split('->').pop();
|
|
1006
|
+
const h = handlerFrameworkOf(mods, resultText);
|
|
1007
|
+
// a handler's result is unit (`ActionM ()`); a guard or helper returns a value (`requireUser :: ActionM User`) and is not a route
|
|
1008
|
+
if (!h || h.framework !== 'scotty' || !/\(\s*\)\s*\)?\s*$/.test(resultText)) continue;
|
|
1009
|
+
add({ framework: 'scotty', method: 'UNKNOWN', path: `<${fun.name}>`, file: mod.file, line: fun.line, mod, handlerName: fun.name, owner: `${mod.name}.${fun.name}`, conventional: true });
|
|
1010
|
+
}
|
|
1011
|
+
}
|
|
1012
|
+
}
|
|
1013
|
+
|
|
930
1014
|
// ── resolve handlers and analyse ─────────────────────────────────────────
|
|
931
1015
|
for (const p of pending) {
|
|
932
1016
|
const R = newRoute({ framework: p.framework, method: p.method, path: p.path, file: p.file, line: p.line, owner: p.owner });
|
|
@@ -967,11 +1051,14 @@ export function analyzeHaskellWeb(files, opts = {}) {
|
|
|
967
1051
|
const g = waiGlobals.find((w) => w.file === p.file && w.routers.has((p.routerFn || '').split('.').pop()));
|
|
968
1052
|
if (g) { o.globalAuth = g; if (g.role) o.globalRole = true; }
|
|
969
1053
|
}
|
|
1054
|
+
// A handler written as a function may take closure arguments BEFORE the endpoint's values (`invoiceById store currentUser invoiceId` for
|
|
1055
|
+
// `Capture "invoiceId" Int :> Get ..`): the endpoint's values are the LAST parameters, so they are paired from the end.
|
|
1056
|
+
const servantOffset = p.framework === 'servant' ? Math.max(0, paramNames.length - (p.servant.order || []).length) : 0;
|
|
970
1057
|
if (p.framework === 'servant' && p.servant.auth) {
|
|
971
1058
|
o.typeAuth = p.servant.auth; if (p.servant.role) o.typeRole = true;
|
|
972
1059
|
o.principalVars = []; o.idVars = [];
|
|
973
1060
|
(p.servant.order || []).forEach((kind, i) => {
|
|
974
|
-
const nm = paramNames[i]; if (!nm) return;
|
|
1061
|
+
const nm = paramNames[i + servantOffset]; if (!nm) return;
|
|
975
1062
|
if (kind === 'auth') o.principalVars.push(nm);
|
|
976
1063
|
else if (/^header:/.test(kind) && CREDENTIAL_HEADER.test(kind.slice(7))) (o.credentialParams ||= []).push({ name: nm, kind: /cookie/i.test(kind) ? 'cookie' : 'token' });
|
|
977
1064
|
else if (/^(?:capture|query):.*id$/i.test(kind)) o.idVars.push(nm);
|
|
@@ -979,7 +1066,7 @@ export function analyzeHaskellWeb(files, opts = {}) {
|
|
|
979
1066
|
} else if (p.framework === 'servant') {
|
|
980
1067
|
o.idVars = [];
|
|
981
1068
|
(p.servant.order || []).forEach((kind, i) => {
|
|
982
|
-
const nm = paramNames[i]; if (!nm) return;
|
|
1069
|
+
const nm = paramNames[i + servantOffset]; if (!nm) return;
|
|
983
1070
|
if (/^(?:capture|query):.*id$/i.test(kind)) o.idVars.push(nm);
|
|
984
1071
|
else if (/^header:/.test(kind) && CREDENTIAL_HEADER.test(kind.slice(7))) (o.credentialParams ||= []).push({ name: nm, kind: /cookie/i.test(kind) ? 'cookie' : 'token' });
|
|
985
1072
|
});
|
|
@@ -1030,6 +1117,9 @@ function collectYesodAuthz(ctx) {
|
|
|
1030
1117
|
const names = [];
|
|
1031
1118
|
for (const n of walkAst(body)) if (n.t === 'var') names.push(n.name);
|
|
1032
1119
|
if (names.some((n) => /^(?:requireAuthId|requireAuth|maybeAuthId|maybeAuth)$/.test(n)) || names.some((n) => { const q = ctx.qualify(mod, { name: n, qual: null }); return q && q.qid && guardSummary(ctx, ctx.fnFor(q)).auth; })) status = 'authenticated';
|
|
1120
|
+
// A clause that can answer AuthenticationRequired or Unauthorized has a rejection path: it decides from something it read (a session
|
|
1121
|
+
// key, a header), so the route is guarded. Only a clause that can ONLY answer Authorized is public.
|
|
1122
|
+
else if (/\b(?:AuthenticationRequired|Unauthorized)\b/.test(text)) status = 'authenticated';
|
|
1033
1123
|
else if (/"Authorized"/.test(text) || names.includes('Authorized')) status = 'public';
|
|
1034
1124
|
if (names.some((n) => ROLE_CALL.test(n))) role = true;
|
|
1035
1125
|
const rec = { status, role, file: mod.file, line: dcl.line || c.line };
|
|
@@ -1071,12 +1161,25 @@ function emitFindings(ctx, result, yesodAuthz) {
|
|
|
1071
1161
|
const writes = WRITE_METHODS.has(R.method) || R.method === 'ANY';
|
|
1072
1162
|
const privileged = ADMIN_PATH.test(R.path) || /admin/i.test(R.handler.name || '');
|
|
1073
1163
|
if (R.auth.status === 'late') emit('hs-route-late-auth', R, { detail: R.auth.evidence[0].order === 'unknown' ? `the rejection ${R.auth.evidence[0].callee} cannot be shown to run before the first sensitive operation (${R.auth.evidence[0].reason})` : `guard ${R.auth.evidence[0].callee} runs after the first sensitive operation`, evidence: R.auth.evidence });
|
|
1074
|
-
else if (R.auth.status === 'none' && (writes || privileged) && R.
|
|
1075
|
-
|
|
1164
|
+
else if (R.auth.status === 'none' && (writes || privileged) && !(R.framework === 'yesod' && R.yesodExplicitPublic && !writes)) {
|
|
1165
|
+
const how = R.method === 'ANY' ? 'any method' : writes ? 'state-changing method' : 'privileged path';
|
|
1166
|
+
if (R.sensitive !== false) emit('hs-route-missing-auth', R, { severity: writes ? 'high' : 'medium', detail: how });
|
|
1167
|
+
// No database, file or process call is visible, but the handler calls something the engine cannot see into (a project function, a
|
|
1168
|
+
// parameter): on a PRIVILEGED path (admin, internal, manage) the route is privileged by its path, so the finding stands at a lower severity
|
|
1169
|
+
// and says what it could not see. A plain write with no visible operation is NOT reported: on real projects that shape is an echo, an
|
|
1170
|
+
// upload demo or a health check (23 such routes on four projects), and only the handler's own text claimed otherwise.
|
|
1171
|
+
else if (privileged && R.effectful) emit('hs-route-missing-auth', R, { severity: 'medium', confidence: 0.55, detail: `${how}; no database, file or process operation is visible in the handler (it calls ${R.effectful.callee}), so the impact is judged from the path alone`, evidence: { unseen: R.effectful.callee, line: R.effectful.line } });
|
|
1076
1172
|
}
|
|
1077
1173
|
if ((R.auth.status === 'authenticated') && privileged && R.role.status !== 'present') emit('hs-route-bfla', R, { detail: 'authenticated but no role/permission check was found', evidence: { auth: R.auth.evidence } });
|
|
1078
|
-
|
|
1079
|
-
|
|
1174
|
+
// Object authorization (BOLA). The lookups are the sensitive calls and the container selections the request id reaches.
|
|
1175
|
+
const lookups = [...(R.idLookups || []), ...(R.idSelections || [])];
|
|
1176
|
+
if (lookups.length && !R.ownership && R.auth.status !== 'late') {
|
|
1177
|
+
const first = lookups[0];
|
|
1178
|
+
if (R.auth.status === 'authenticated') emit('hs-route-bola', R, { detail: 'the id from the request reaches a lookup with no ownership or tenant check', line: first.line, evidence: { lookup: first.call.callee, ...(R.ownerEvidence ? { owner: R.ownerEvidence } : {}) } });
|
|
1179
|
+
// Unauthenticated: a WRITE to an object the client selected is the client's to make only if it owns the object, and nothing checked; for a
|
|
1180
|
+
// read, the finding needs evidence that the object has an owner at all (a public article by id is not an authorization matter).
|
|
1181
|
+
else if (writes) emit('hs-route-bola', R, { detail: 'the id from the request selects the object that is modified, with no authenticated principal and no ownership check', line: first.line, evidence: { lookup: first.call.callee, ...(R.ownerEvidence ? { owner: R.ownerEvidence } : {}) } });
|
|
1182
|
+
else if (R.ownerEvidence) emit('hs-route-bola', R, { detail: R.ownerEvidence.kind === 'owner-column-not-in-where' ? `the statement names an owner column (${R.ownerEvidence.column}) that its WHERE clause does not use: the object is selected by the client's id alone` : `the ${R.ownerEvidence.type} record has an owner field (${R.ownerEvidence.field}) the handler never reads: the object is selected by the client's id alone`, line: first.line, evidence: { lookup: first.call.callee, owner: R.ownerEvidence } });
|
|
1080
1183
|
}
|
|
1081
1184
|
const cookie = R.credentialKinds && (R.credentialKinds.has('cookie') || R.credentialKinds.has('session'));
|
|
1082
1185
|
if (writes && R.auth.status === 'authenticated' && cookie && !R.csrf && R.framework !== 'yesod') emit('hs-route-csrf', R, { detail: 'cookie/session credential with no CSRF token check' });
|
|
@@ -57,6 +57,7 @@ import { CATALOG } from '../dataflow/catalog.js';
|
|
|
57
57
|
export const PROVENANCE_MAP = Object.freeze({
|
|
58
58
|
'http-body': Object.freeze({ category: 'http-body', status: 'modeled', why: 'exact vocabulary match' }),
|
|
59
59
|
'url-param': Object.freeze({ category: 'http-query', status: 'modeled', why: 'pure rename; the query string' }),
|
|
60
|
+
'upload': Object.freeze({ category: 'http-upload', status: 'modeled', why: 'a field of an uploaded file the client wrote into the multipart body (its name, content type or content); 0.195.8, Haskell wai-extra' }),
|
|
60
61
|
'path-param': Object.freeze({ category: 'http-route', status: 'modeled', why: 'pure rename; a route/path segment' }),
|
|
61
62
|
'header': Object.freeze({ category: 'http-header', status: 'modeled', why: 'pure rename' }),
|
|
62
63
|
'cookie': Object.freeze({ category: 'http-cookie', status: 'modeled', why: 'pure rename' }),
|