circle-ir 3.181.0 → 3.185.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.
- package/dist/analysis/config-loader.d.ts.map +1 -1
- package/dist/analysis/config-loader.js +97 -0
- package/dist/analysis/config-loader.js.map +1 -1
- package/dist/analysis/passes/language-sources-pass.d.ts.map +1 -1
- package/dist/analysis/passes/language-sources-pass.js +207 -0
- package/dist/analysis/passes/language-sources-pass.js.map +1 -1
- package/dist/analysis/passes/taint-propagation-pass.d.ts.map +1 -1
- package/dist/analysis/passes/taint-propagation-pass.js +158 -0
- package/dist/analysis/passes/taint-propagation-pass.js.map +1 -1
- package/dist/analysis/taint-matcher.js +27 -1
- package/dist/analysis/taint-matcher.js.map +1 -1
- package/dist/analysis/taint-propagation.d.ts.map +1 -1
- package/dist/analysis/taint-propagation.js +47 -11
- package/dist/analysis/taint-propagation.js.map +1 -1
- package/dist/browser/circle-ir.js +413 -10
- package/dist/core/circle-ir-core.cjs +189 -9
- package/dist/core/circle-ir-core.js +189 -9
- package/dist/core/extractors/dfg.js +99 -0
- package/dist/core/extractors/dfg.js.map +1 -1
- package/package.json +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"language-sources-pass.d.ts","sourceRoot":"","sources":["../../../src/analysis/passes/language-sources-pass.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAEH,OAAO,KAAK,EAAE,WAAW,EAAE,SAAS,EAAE,cAAc,EAAwB,WAAW,EAAO,MAAM,sBAAsB,CAAC;AAC3H,OAAO,KAAK,EAAE,YAAY,EAAE,WAAW,EAAE,MAAM,8BAA8B,CAAC;AAqB9E,eAAO,MAAM,mBAAmB;;;;;;;;;;;;;;;;;;;;;;;;;;;IAsD/B,CAAC;
|
|
1
|
+
{"version":3,"file":"language-sources-pass.d.ts","sourceRoot":"","sources":["../../../src/analysis/passes/language-sources-pass.ts"],"names":[],"mappings":"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"}
|
|
@@ -108,6 +108,21 @@ const PYTHON_TAINTED_PATTERNS = [
|
|
|
108
108
|
// registry, so `name = input()` was not added to pyTaintedVars. Closes
|
|
109
109
|
// the deferred `getattr(obj, input())()` reflection-invocation shape.
|
|
110
110
|
{ pattern: /\binput\s*\(/, type: 'io_input' },
|
|
111
|
+
// WebSocket transport channels (cognium-dev #213 second slice).
|
|
112
|
+
// FastAPI / Starlette (`from fastapi import WebSocket`) and Django
|
|
113
|
+
// Channels consumers both expose `receive_*` methods that return
|
|
114
|
+
// untrusted frame payloads on every call. Registered in
|
|
115
|
+
// config-loader.ts as return-tainted; the forward-taint regex here
|
|
116
|
+
// enables `data = await websocket.receive_text()` → `data` tainted,
|
|
117
|
+
// which the DFG-less Python path needs so downstream sinks that
|
|
118
|
+
// consume `data` are flagged.
|
|
119
|
+
//
|
|
120
|
+
// Deliberately not adding a bare `.receive\s*\(` here — that would
|
|
121
|
+
// match too many unrelated APIs (queue receive, signal receive, etc.)
|
|
122
|
+
// and produce spurious sources on every `x = q.receive()` in the wild.
|
|
123
|
+
{ pattern: /\.receive_text\s*\(/, type: 'network_input' },
|
|
124
|
+
{ pattern: /\.receive_bytes\s*\(/, type: 'network_input' },
|
|
125
|
+
{ pattern: /\.receive_json\s*\(/, type: 'http_body' },
|
|
111
126
|
];
|
|
112
127
|
// ---------------------------------------------------------------------------
|
|
113
128
|
// Pass
|
|
@@ -1013,6 +1028,73 @@ function findJavaScriptAssignmentSources(sourceCode, language) {
|
|
|
1013
1028
|
}
|
|
1014
1029
|
}
|
|
1015
1030
|
}
|
|
1031
|
+
// WebSocket / EventEmitter callback-parameter sources (cognium-dev #213).
|
|
1032
|
+
//
|
|
1033
|
+
// ws / Socket.IO / Node.js EventEmitter idiomatic pattern:
|
|
1034
|
+
// ws.on('message', (data) => sink(data))
|
|
1035
|
+
// socket.on('message', function(payload) { sink(payload) })
|
|
1036
|
+
// ws.on('message', async (data) => sink(data))
|
|
1037
|
+
//
|
|
1038
|
+
// The FIRST parameter of the callback is the attacker-authored frame
|
|
1039
|
+
// payload for the 'message' / 'text' / 'binary' events. Scoped by
|
|
1040
|
+
// event-name allowlist to avoid firing on unrelated `.on('drain', cb)`
|
|
1041
|
+
// / `.on('close', cb)` etc. that carry non-user-input.
|
|
1042
|
+
sources.push(...findJavaScriptCallbackParamSources(sourceCode));
|
|
1043
|
+
return sources;
|
|
1044
|
+
}
|
|
1045
|
+
/**
|
|
1046
|
+
* cognium-dev #213 — JS/TS WebSocket callback-parameter sources.
|
|
1047
|
+
*
|
|
1048
|
+
* Scans for `<receiver>.on('<event>', <callback>)` where <event> is one
|
|
1049
|
+
* of the WebSocket message events, and extracts the callback's first
|
|
1050
|
+
* parameter name. Registers a `network_input` source anchored at the
|
|
1051
|
+
* callback's declaration line with `variable: <param-name>` so
|
|
1052
|
+
* downstream sinks that consume that variable are flagged.
|
|
1053
|
+
*
|
|
1054
|
+
* Event names allowlisted: `message`, `text`, `binary`. These are
|
|
1055
|
+
* unambiguously frame-payload events across ws / Socket.IO / uWS /
|
|
1056
|
+
* @fastify/websocket. Deliberately does NOT include `data`
|
|
1057
|
+
* (Node.js streams reuse this name for legit non-attacker sources)
|
|
1058
|
+
* or `connection` (payload is a socket handle, not user input).
|
|
1059
|
+
*
|
|
1060
|
+
* Note: `close` / `open` / `error` / `drain` etc. are intentionally
|
|
1061
|
+
* out of scope — their callback params are lifecycle metadata, not
|
|
1062
|
+
* user input, and firing on them would swamp legit event-driven code
|
|
1063
|
+
* with FPs.
|
|
1064
|
+
*/
|
|
1065
|
+
function findJavaScriptCallbackParamSources(sourceCode) {
|
|
1066
|
+
const sources = [];
|
|
1067
|
+
// Match `.on('message', <callback-first-arg>)` where callback is either:
|
|
1068
|
+
// - arrow function: `(name)` / `(name,` / `name =>` / `async (name)`
|
|
1069
|
+
// - function expr: `function (name)` / `function name(name)` /
|
|
1070
|
+
// `async function (name)` / `function* (name)`
|
|
1071
|
+
const CB_EVENT_RE = /\.on\s*\(\s*['"](?:message|text|binary)['"]\s*,\s*(?:async\s+)?(?:function\s*\*?\s*(?:\w+\s*)?\(\s*(\w+)|\(\s*(\w+)\s*(?:[,):]|$)|(\w+)\s*=>)/;
|
|
1072
|
+
const lines = sourceCode.split('\n');
|
|
1073
|
+
for (let i = 0; i < lines.length; i++) {
|
|
1074
|
+
const line = lines[i];
|
|
1075
|
+
const trimmed = line.trimStart();
|
|
1076
|
+
if (trimmed.startsWith('//') || trimmed.startsWith('*'))
|
|
1077
|
+
continue;
|
|
1078
|
+
const m = line.match(CB_EVENT_RE);
|
|
1079
|
+
if (!m)
|
|
1080
|
+
continue;
|
|
1081
|
+
const paramName = m[1] ?? m[2] ?? m[3];
|
|
1082
|
+
if (!paramName)
|
|
1083
|
+
continue;
|
|
1084
|
+
// Skip conventional lifecycle-arg names that would poison downstream
|
|
1085
|
+
// scans with FPs if treated as tainted. `err` / `error` are almost
|
|
1086
|
+
// never actual frame payloads; keep them out even inside 'message'.
|
|
1087
|
+
if (paramName === 'err' || paramName === 'error')
|
|
1088
|
+
continue;
|
|
1089
|
+
sources.push({
|
|
1090
|
+
type: 'network_input',
|
|
1091
|
+
location: `WebSocket .on(...) callback param '${paramName}' at line ${i + 1}`,
|
|
1092
|
+
severity: 'high',
|
|
1093
|
+
line: i + 1,
|
|
1094
|
+
confidence: 0.95,
|
|
1095
|
+
variable: paramName,
|
|
1096
|
+
});
|
|
1097
|
+
}
|
|
1016
1098
|
return sources;
|
|
1017
1099
|
}
|
|
1018
1100
|
function findPythonAssignmentSources(sourceCode, language) {
|
|
@@ -1398,6 +1480,18 @@ export function buildJavaScriptTaintedVars(sourceCode, language) {
|
|
|
1398
1480
|
return new Map();
|
|
1399
1481
|
const tainted = new Map();
|
|
1400
1482
|
const lines = sourceCode.split('\n');
|
|
1483
|
+
// Seed with WebSocket `.on('message', cb)` callback-param names (#213
|
|
1484
|
+
// second slice). Same event allowlist as findJavaScriptCallbackParamSources.
|
|
1485
|
+
const CB_EVENT_RE = /\.on\s*\(\s*['"](?:message|text|binary)['"]\s*,\s*(?:async\s+)?(?:function\s*\*?\s*(?:\w+\s*)?\(\s*(\w+)|\(\s*(\w+)\s*(?:[,):]|$)|(\w+)\s*=>)/;
|
|
1486
|
+
for (let i = 0; i < lines.length; i++) {
|
|
1487
|
+
const m = lines[i].match(CB_EVENT_RE);
|
|
1488
|
+
if (!m)
|
|
1489
|
+
continue;
|
|
1490
|
+
const paramName = m[1] ?? m[2] ?? m[3];
|
|
1491
|
+
if (!paramName || paramName === 'err' || paramName === 'error')
|
|
1492
|
+
continue;
|
|
1493
|
+
tainted.set(paramName, i + 1);
|
|
1494
|
+
}
|
|
1401
1495
|
for (let i = 0; i < lines.length; i++) {
|
|
1402
1496
|
const line = lines[i];
|
|
1403
1497
|
const trimmed = line.trimStart();
|
|
@@ -1671,6 +1765,119 @@ function findBashTaintSources(sourceCode, dfg) {
|
|
|
1671
1765
|
});
|
|
1672
1766
|
}
|
|
1673
1767
|
}
|
|
1768
|
+
// 2b. `read` builtin — stdin sources (cognium-dev #213 third slice).
|
|
1769
|
+
//
|
|
1770
|
+
// read x → $x is tainted (stdin)
|
|
1771
|
+
// read -r x → $x is tainted (raw mode)
|
|
1772
|
+
// read -p "prompt" x → $x is tainted (prompted read)
|
|
1773
|
+
// read x y z → $x, $y, $z are all tainted
|
|
1774
|
+
// read → $REPLY is tainted
|
|
1775
|
+
// read -a arr → $arr array tainted
|
|
1776
|
+
// mapfile -t arr / readarray arr → $arr array tainted
|
|
1777
|
+
// getopts "abc" flag → $flag is tainted, and $OPTARG carries values
|
|
1778
|
+
//
|
|
1779
|
+
// All are stdin/CLI-driven and untrusted the moment they land. Treat
|
|
1780
|
+
// as `io_input` (same bucket as positional params for reach-map).
|
|
1781
|
+
// Exclude `read()` function definitions and anything containing `(`
|
|
1782
|
+
// to avoid firing on `read() {` / `read () {` etc.
|
|
1783
|
+
const readMatch = /^read\s*\(/.test(trimmed) ? null : trimmed.match(/^read\b([^#(]*)$/);
|
|
1784
|
+
if (readMatch) {
|
|
1785
|
+
const argStr = readMatch[1].trim();
|
|
1786
|
+
// Strip trailing redirect (`read x < file` — the file case is still stdin-shaped)
|
|
1787
|
+
const noRedirect = argStr.replace(/\s*<[^<].*$/, '').trim();
|
|
1788
|
+
// Extract identifier operands (skip -flags and their string args).
|
|
1789
|
+
const tokens = noRedirect.split(/\s+/);
|
|
1790
|
+
const varNames = [];
|
|
1791
|
+
for (let ti = 0; ti < tokens.length; ti++) {
|
|
1792
|
+
const tok = tokens[ti];
|
|
1793
|
+
if (!tok)
|
|
1794
|
+
continue;
|
|
1795
|
+
if (tok.startsWith('-')) {
|
|
1796
|
+
// Flags that take an argument consume the next token
|
|
1797
|
+
// (a=array-name, n/N=nchars, p=prompt, t=timeout, i=text,
|
|
1798
|
+
// d=delim, u=fd). `-r` / `-s` / `-e` take no argument.
|
|
1799
|
+
if (/^-[antpidu]$|^-N$/.test(tok))
|
|
1800
|
+
ti++;
|
|
1801
|
+
continue;
|
|
1802
|
+
}
|
|
1803
|
+
if (/^[A-Za-z_][\w]*$/.test(tok))
|
|
1804
|
+
varNames.push(tok);
|
|
1805
|
+
}
|
|
1806
|
+
// No explicit vars → $REPLY holds the line.
|
|
1807
|
+
if (varNames.length === 0)
|
|
1808
|
+
varNames.push('REPLY');
|
|
1809
|
+
for (const v of varNames) {
|
|
1810
|
+
const already = sources.some(s => s.line === lineNumber && s.variable === v);
|
|
1811
|
+
if (already)
|
|
1812
|
+
continue;
|
|
1813
|
+
sources.push({
|
|
1814
|
+
type: 'io_input',
|
|
1815
|
+
location: `read → $${v} (stdin)`,
|
|
1816
|
+
severity: 'high',
|
|
1817
|
+
line: lineNumber,
|
|
1818
|
+
confidence: 0.9,
|
|
1819
|
+
variable: v,
|
|
1820
|
+
});
|
|
1821
|
+
}
|
|
1822
|
+
}
|
|
1823
|
+
// mapfile / readarray → whole array from stdin
|
|
1824
|
+
const mapfileMatch = /^(?:mapfile|readarray)\s*\(/.test(trimmed)
|
|
1825
|
+
? null
|
|
1826
|
+
: trimmed.match(/^(?:mapfile|readarray)\b([^#(]*)$/);
|
|
1827
|
+
if (mapfileMatch) {
|
|
1828
|
+
const argStr = mapfileMatch[1].trim();
|
|
1829
|
+
const tokens = argStr.split(/\s+/);
|
|
1830
|
+
const varNames = [];
|
|
1831
|
+
for (let ti = 0; ti < tokens.length; ti++) {
|
|
1832
|
+
const tok = tokens[ti];
|
|
1833
|
+
if (!tok)
|
|
1834
|
+
continue;
|
|
1835
|
+
if (tok.startsWith('-')) {
|
|
1836
|
+
// Arg-taking flags: c=chunk, C=callback, n=max, O=origin,
|
|
1837
|
+
// s=skip, u=fd, d=delim. `-t` takes no argument.
|
|
1838
|
+
if (/^-[cCnOsud]$/.test(tok))
|
|
1839
|
+
ti++;
|
|
1840
|
+
continue;
|
|
1841
|
+
}
|
|
1842
|
+
if (/^[A-Za-z_][\w]*$/.test(tok))
|
|
1843
|
+
varNames.push(tok);
|
|
1844
|
+
}
|
|
1845
|
+
if (varNames.length === 0)
|
|
1846
|
+
varNames.push('MAPFILE');
|
|
1847
|
+
for (const v of varNames) {
|
|
1848
|
+
const already = sources.some(s => s.line === lineNumber && s.variable === v);
|
|
1849
|
+
if (already)
|
|
1850
|
+
continue;
|
|
1851
|
+
sources.push({
|
|
1852
|
+
type: 'io_input',
|
|
1853
|
+
location: `mapfile → $${v} (stdin array)`,
|
|
1854
|
+
severity: 'high',
|
|
1855
|
+
line: lineNumber,
|
|
1856
|
+
confidence: 0.9,
|
|
1857
|
+
variable: v,
|
|
1858
|
+
});
|
|
1859
|
+
}
|
|
1860
|
+
}
|
|
1861
|
+
// getopts option-parsing loop: `while getopts "abc" flag; do ...`
|
|
1862
|
+
// → $flag holds the current option letter (attacker-chosen CLI arg)
|
|
1863
|
+
// → $OPTARG holds the option's value on `:` flags
|
|
1864
|
+
const getoptsMatch = trimmed.match(/\bgetopts\s+["'][^"']+["']\s+(\w+)/);
|
|
1865
|
+
if (getoptsMatch) {
|
|
1866
|
+
const flagVar = getoptsMatch[1];
|
|
1867
|
+
for (const v of [flagVar, 'OPTARG']) {
|
|
1868
|
+
const already = sources.some(s => s.line === lineNumber && s.variable === v);
|
|
1869
|
+
if (already)
|
|
1870
|
+
continue;
|
|
1871
|
+
sources.push({
|
|
1872
|
+
type: 'io_input',
|
|
1873
|
+
location: `getopts → $${v} (CLI arg)`,
|
|
1874
|
+
severity: 'high',
|
|
1875
|
+
line: lineNumber,
|
|
1876
|
+
confidence: 0.9,
|
|
1877
|
+
variable: v,
|
|
1878
|
+
});
|
|
1879
|
+
}
|
|
1880
|
+
}
|
|
1674
1881
|
// 3. Environment variables: $VAR where VAR was never assigned in the script
|
|
1675
1882
|
// and matches known untrusted env patterns
|
|
1676
1883
|
const envRe = /\$([A-Z][A-Z0-9_]{2,})|\$\{([A-Z][A-Z0-9_]{2,})\}/g;
|