circle-ir 3.1.0 → 3.2.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/README.md +3 -0
- package/configs/sinks/code_injection.yaml +136 -0
- package/configs/sinks/command.yaml +109 -0
- package/configs/sinks/javascript_dom_xss.yaml +131 -0
- package/configs/sinks/path.yaml +113 -0
- package/configs/sources/http_sources.yaml +151 -0
- package/configs/sources/javascript_http.yaml +296 -0
- package/configs/sources/python.json +78 -0
- package/dist/analysis/taint-matcher.js +71 -13
- package/dist/analysis/taint-matcher.js.map +1 -1
- package/dist/analyzer.js +218 -1
- package/dist/analyzer.js.map +1 -1
- package/dist/browser/circle-ir.js +549 -16
- package/dist/core/circle-ir-core.cjs +45 -14
- package/dist/core/circle-ir-core.js +45 -14
- package/dist/languages/plugins/javascript.js +333 -1
- package/dist/languages/plugins/javascript.js.map +1 -1
- package/dist/types/index.d.ts +1 -1
- package/package.json +2 -1
|
@@ -10175,15 +10175,16 @@ function findSources(calls, types, patterns) {
|
|
|
10175
10175
|
const skipMethods = ["toString", "hashCode", "equals", "compareTo", "getDescription", "getVulnerabilityCount"];
|
|
10176
10176
|
if (skipMethods.includes(method.name)) continue;
|
|
10177
10177
|
for (const param of method.parameters) {
|
|
10178
|
-
|
|
10178
|
+
const isTaintable = param.type ? isInterproceduralTaintableType(param.type) : true;
|
|
10179
|
+
if (isTaintable) {
|
|
10179
10180
|
const paramLine = param.line ?? method.start_line;
|
|
10180
10181
|
sources.push({
|
|
10181
10182
|
type: "interprocedural_param",
|
|
10182
|
-
location: `${param.type} ${param.name} in ${method.name}`,
|
|
10183
|
+
location: `${param.type || "any"} ${param.name} in ${method.name}`,
|
|
10183
10184
|
severity: "medium",
|
|
10184
10185
|
line: paramLine,
|
|
10185
|
-
confidence: 0.7
|
|
10186
|
-
// Lower confidence
|
|
10186
|
+
confidence: param.type ? 0.7 : 0.5
|
|
10187
|
+
// Lower confidence for untyped params
|
|
10187
10188
|
});
|
|
10188
10189
|
}
|
|
10189
10190
|
}
|
|
@@ -10209,7 +10210,15 @@ function findSources(calls, types, patterns) {
|
|
|
10209
10210
|
}
|
|
10210
10211
|
}
|
|
10211
10212
|
}
|
|
10212
|
-
|
|
10213
|
+
const sourceMap = /* @__PURE__ */ new Map();
|
|
10214
|
+
for (const source of sources) {
|
|
10215
|
+
const key = `${source.line}:${source.type}`;
|
|
10216
|
+
const existing = sourceMap.get(key);
|
|
10217
|
+
if (!existing || source.confidence > existing.confidence) {
|
|
10218
|
+
sourceMap.set(key, source);
|
|
10219
|
+
}
|
|
10220
|
+
}
|
|
10221
|
+
return Array.from(sourceMap.values());
|
|
10213
10222
|
}
|
|
10214
10223
|
function isInterproceduralTaintableType(typeName) {
|
|
10215
10224
|
const baseType = typeName.split("<")[0].trim();
|
|
@@ -10292,22 +10301,44 @@ function isInterproceduralTaintableType(typeName) {
|
|
|
10292
10301
|
}
|
|
10293
10302
|
return false;
|
|
10294
10303
|
}
|
|
10304
|
+
function isParameterizedQueryCall(call, pattern) {
|
|
10305
|
+
if (pattern.type !== "sql_injection") return false;
|
|
10306
|
+
if (call.arguments.length < 2) return false;
|
|
10307
|
+
const secondArg = call.arguments.find((a) => a.position === 1);
|
|
10308
|
+
if (!secondArg) return false;
|
|
10309
|
+
if (secondArg.expression) {
|
|
10310
|
+
const expr = secondArg.expression.trim();
|
|
10311
|
+
if (expr.startsWith("[")) {
|
|
10312
|
+
return true;
|
|
10313
|
+
}
|
|
10314
|
+
}
|
|
10315
|
+
return false;
|
|
10316
|
+
}
|
|
10295
10317
|
function findSinks(calls, patterns) {
|
|
10296
|
-
const
|
|
10318
|
+
const sinkMap = /* @__PURE__ */ new Map();
|
|
10297
10319
|
for (const call of calls) {
|
|
10298
10320
|
for (const pattern of patterns) {
|
|
10299
10321
|
if (matchesSinkPattern(call, pattern)) {
|
|
10300
|
-
|
|
10301
|
-
|
|
10302
|
-
|
|
10303
|
-
|
|
10304
|
-
|
|
10305
|
-
|
|
10306
|
-
|
|
10322
|
+
if (isParameterizedQueryCall(call, pattern)) {
|
|
10323
|
+
continue;
|
|
10324
|
+
}
|
|
10325
|
+
const location = formatCallLocation(call);
|
|
10326
|
+
const key = `${location}:${call.location.line}:${pattern.cwe}`;
|
|
10327
|
+
const confidence = calculateSinkConfidence(call, pattern);
|
|
10328
|
+
const existing = sinkMap.get(key);
|
|
10329
|
+
if (!existing || confidence > existing.confidence) {
|
|
10330
|
+
sinkMap.set(key, {
|
|
10331
|
+
type: pattern.type,
|
|
10332
|
+
cwe: pattern.cwe,
|
|
10333
|
+
location,
|
|
10334
|
+
line: call.location.line,
|
|
10335
|
+
confidence
|
|
10336
|
+
});
|
|
10337
|
+
}
|
|
10307
10338
|
}
|
|
10308
10339
|
}
|
|
10309
10340
|
}
|
|
10310
|
-
return
|
|
10341
|
+
return Array.from(sinkMap.values());
|
|
10311
10342
|
}
|
|
10312
10343
|
function matchesSourcePattern(call, pattern) {
|
|
10313
10344
|
if (pattern.method) {
|
|
@@ -14269,16 +14300,36 @@ var JavaScriptPlugin = class extends BaseLanguagePlugin {
|
|
|
14269
14300
|
confidence = Math.max(confidence, 0.95);
|
|
14270
14301
|
indicators.push(`import: ${path}`);
|
|
14271
14302
|
}
|
|
14272
|
-
if (path === "react" || path.startsWith("react/")) {
|
|
14303
|
+
if (path === "react" || path.startsWith("react/") || path === "react-dom") {
|
|
14273
14304
|
framework = framework || "react";
|
|
14274
14305
|
confidence = Math.max(confidence, 0.8);
|
|
14275
14306
|
indicators.push(`import: ${path}`);
|
|
14276
14307
|
}
|
|
14308
|
+
if (path === "react-native" || path.startsWith("react-native/") || path.startsWith("@react-native/")) {
|
|
14309
|
+
framework = "react-native";
|
|
14310
|
+
confidence = Math.max(confidence, 0.95);
|
|
14311
|
+
indicators.push(`import: ${path}`);
|
|
14312
|
+
}
|
|
14313
|
+
if (path.startsWith("@react-navigation/")) {
|
|
14314
|
+
framework = framework || "react-native";
|
|
14315
|
+
confidence = Math.max(confidence, 0.9);
|
|
14316
|
+
indicators.push(`import: ${path}`);
|
|
14317
|
+
}
|
|
14318
|
+
if (path === "react-router" || path === "react-router-dom" || path.startsWith("react-router/")) {
|
|
14319
|
+
framework = framework || "react";
|
|
14320
|
+
confidence = Math.max(confidence, 0.85);
|
|
14321
|
+
indicators.push(`import: ${path}`);
|
|
14322
|
+
}
|
|
14277
14323
|
if (path === "next" || path.startsWith("next/")) {
|
|
14278
14324
|
framework = "nextjs";
|
|
14279
14325
|
confidence = Math.max(confidence, 0.9);
|
|
14280
14326
|
indicators.push(`import: ${path}`);
|
|
14281
14327
|
}
|
|
14328
|
+
if (path === "expo" || path.startsWith("expo-")) {
|
|
14329
|
+
framework = framework || "react-native";
|
|
14330
|
+
confidence = Math.max(confidence, 0.85);
|
|
14331
|
+
indicators.push(`import: ${path}`);
|
|
14332
|
+
}
|
|
14282
14333
|
}
|
|
14283
14334
|
if (framework) {
|
|
14284
14335
|
return { name: framework, confidence, indicators };
|
|
@@ -14390,6 +14441,167 @@ var JavaScriptPlugin = class extends BaseLanguagePlugin {
|
|
|
14390
14441
|
severity: "medium",
|
|
14391
14442
|
confidence: 0.8,
|
|
14392
14443
|
returnTainted: true
|
|
14444
|
+
},
|
|
14445
|
+
// =========================================================
|
|
14446
|
+
// React Router Sources
|
|
14447
|
+
// =========================================================
|
|
14448
|
+
{
|
|
14449
|
+
method: "useParams",
|
|
14450
|
+
type: "http_path",
|
|
14451
|
+
severity: "high",
|
|
14452
|
+
confidence: 0.95,
|
|
14453
|
+
returnTainted: true
|
|
14454
|
+
},
|
|
14455
|
+
{
|
|
14456
|
+
method: "useSearchParams",
|
|
14457
|
+
type: "http_param",
|
|
14458
|
+
severity: "high",
|
|
14459
|
+
confidence: 0.95,
|
|
14460
|
+
returnTainted: true
|
|
14461
|
+
},
|
|
14462
|
+
{
|
|
14463
|
+
method: "useLocation",
|
|
14464
|
+
type: "url_param",
|
|
14465
|
+
severity: "high",
|
|
14466
|
+
confidence: 0.9,
|
|
14467
|
+
returnTainted: true
|
|
14468
|
+
},
|
|
14469
|
+
// =========================================================
|
|
14470
|
+
// Next.js Sources
|
|
14471
|
+
// =========================================================
|
|
14472
|
+
{
|
|
14473
|
+
method: "useRouter",
|
|
14474
|
+
type: "http_param",
|
|
14475
|
+
severity: "high",
|
|
14476
|
+
confidence: 0.9,
|
|
14477
|
+
returnTainted: true
|
|
14478
|
+
// router.query, router.asPath
|
|
14479
|
+
},
|
|
14480
|
+
{
|
|
14481
|
+
method: "useSearchParams",
|
|
14482
|
+
// Next.js App Router
|
|
14483
|
+
type: "http_param",
|
|
14484
|
+
severity: "high",
|
|
14485
|
+
confidence: 0.95,
|
|
14486
|
+
returnTainted: true
|
|
14487
|
+
},
|
|
14488
|
+
{
|
|
14489
|
+
method: "usePathname",
|
|
14490
|
+
type: "http_path",
|
|
14491
|
+
severity: "high",
|
|
14492
|
+
confidence: 0.9,
|
|
14493
|
+
returnTainted: true
|
|
14494
|
+
},
|
|
14495
|
+
{
|
|
14496
|
+
// getServerSideProps/getStaticProps context.params
|
|
14497
|
+
method: "params",
|
|
14498
|
+
type: "http_path",
|
|
14499
|
+
severity: "high",
|
|
14500
|
+
confidence: 0.85,
|
|
14501
|
+
returnTainted: true
|
|
14502
|
+
},
|
|
14503
|
+
// =========================================================
|
|
14504
|
+
// React Native Sources
|
|
14505
|
+
// =========================================================
|
|
14506
|
+
{
|
|
14507
|
+
// React Navigation route params
|
|
14508
|
+
method: "useRoute",
|
|
14509
|
+
type: "navigation_param",
|
|
14510
|
+
severity: "high",
|
|
14511
|
+
confidence: 0.9,
|
|
14512
|
+
returnTainted: true
|
|
14513
|
+
},
|
|
14514
|
+
{
|
|
14515
|
+
// Deep linking
|
|
14516
|
+
method: "getInitialURL",
|
|
14517
|
+
class: "Linking",
|
|
14518
|
+
type: "url_param",
|
|
14519
|
+
severity: "high",
|
|
14520
|
+
confidence: 0.95,
|
|
14521
|
+
returnTainted: true
|
|
14522
|
+
},
|
|
14523
|
+
{
|
|
14524
|
+
method: "addEventListener",
|
|
14525
|
+
class: "Linking",
|
|
14526
|
+
type: "url_param",
|
|
14527
|
+
severity: "high",
|
|
14528
|
+
confidence: 0.9,
|
|
14529
|
+
returnTainted: true
|
|
14530
|
+
},
|
|
14531
|
+
{
|
|
14532
|
+
method: "parse",
|
|
14533
|
+
class: "Linking",
|
|
14534
|
+
type: "url_param",
|
|
14535
|
+
severity: "high",
|
|
14536
|
+
confidence: 0.9,
|
|
14537
|
+
returnTainted: true
|
|
14538
|
+
},
|
|
14539
|
+
{
|
|
14540
|
+
// Clipboard content
|
|
14541
|
+
method: "getString",
|
|
14542
|
+
class: "Clipboard",
|
|
14543
|
+
type: "user_input",
|
|
14544
|
+
severity: "medium",
|
|
14545
|
+
confidence: 0.85,
|
|
14546
|
+
returnTainted: true
|
|
14547
|
+
},
|
|
14548
|
+
{
|
|
14549
|
+
method: "getStringAsync",
|
|
14550
|
+
class: "Clipboard",
|
|
14551
|
+
type: "user_input",
|
|
14552
|
+
severity: "medium",
|
|
14553
|
+
confidence: 0.85,
|
|
14554
|
+
returnTainted: true
|
|
14555
|
+
},
|
|
14556
|
+
{
|
|
14557
|
+
// AsyncStorage (may contain user data)
|
|
14558
|
+
method: "getItem",
|
|
14559
|
+
class: "AsyncStorage",
|
|
14560
|
+
type: "storage_input",
|
|
14561
|
+
severity: "medium",
|
|
14562
|
+
confidence: 0.7,
|
|
14563
|
+
returnTainted: true
|
|
14564
|
+
},
|
|
14565
|
+
{
|
|
14566
|
+
method: "multiGet",
|
|
14567
|
+
class: "AsyncStorage",
|
|
14568
|
+
type: "storage_input",
|
|
14569
|
+
severity: "medium",
|
|
14570
|
+
confidence: 0.7,
|
|
14571
|
+
returnTainted: true
|
|
14572
|
+
},
|
|
14573
|
+
{
|
|
14574
|
+
// SecureStore (Expo)
|
|
14575
|
+
method: "getItemAsync",
|
|
14576
|
+
class: "SecureStore",
|
|
14577
|
+
type: "storage_input",
|
|
14578
|
+
severity: "medium",
|
|
14579
|
+
confidence: 0.7,
|
|
14580
|
+
returnTainted: true
|
|
14581
|
+
},
|
|
14582
|
+
// =========================================================
|
|
14583
|
+
// Browser/DOM Sources (React web apps)
|
|
14584
|
+
// =========================================================
|
|
14585
|
+
{
|
|
14586
|
+
method: "localStorage.getItem",
|
|
14587
|
+
type: "storage_input",
|
|
14588
|
+
severity: "medium",
|
|
14589
|
+
confidence: 0.7,
|
|
14590
|
+
returnTainted: true
|
|
14591
|
+
},
|
|
14592
|
+
{
|
|
14593
|
+
method: "sessionStorage.getItem",
|
|
14594
|
+
type: "storage_input",
|
|
14595
|
+
severity: "medium",
|
|
14596
|
+
confidence: 0.7,
|
|
14597
|
+
returnTainted: true
|
|
14598
|
+
},
|
|
14599
|
+
{
|
|
14600
|
+
method: "postMessage",
|
|
14601
|
+
type: "message_input",
|
|
14602
|
+
severity: "high",
|
|
14603
|
+
confidence: 0.85,
|
|
14604
|
+
returnTainted: true
|
|
14393
14605
|
}
|
|
14394
14606
|
];
|
|
14395
14607
|
}
|
|
@@ -14569,6 +14781,158 @@ var JavaScriptPlugin = class extends BaseLanguagePlugin {
|
|
|
14569
14781
|
cwe: "CWE-1321",
|
|
14570
14782
|
severity: "high",
|
|
14571
14783
|
argPositions: [0, 1]
|
|
14784
|
+
},
|
|
14785
|
+
// =========================================================
|
|
14786
|
+
// React XSS Sinks
|
|
14787
|
+
// =========================================================
|
|
14788
|
+
{
|
|
14789
|
+
// Most common React XSS vector
|
|
14790
|
+
method: "dangerouslySetInnerHTML",
|
|
14791
|
+
type: "xss",
|
|
14792
|
+
cwe: "CWE-79",
|
|
14793
|
+
severity: "critical",
|
|
14794
|
+
argPositions: [0]
|
|
14795
|
+
// The __html property value
|
|
14796
|
+
},
|
|
14797
|
+
{
|
|
14798
|
+
// Rendering user-controlled href with javascript:
|
|
14799
|
+
method: "href",
|
|
14800
|
+
type: "xss",
|
|
14801
|
+
cwe: "CWE-79",
|
|
14802
|
+
severity: "high",
|
|
14803
|
+
argPositions: [0]
|
|
14804
|
+
},
|
|
14805
|
+
{
|
|
14806
|
+
// createRef().current.innerHTML
|
|
14807
|
+
method: "current.innerHTML",
|
|
14808
|
+
type: "xss",
|
|
14809
|
+
cwe: "CWE-79",
|
|
14810
|
+
severity: "high",
|
|
14811
|
+
argPositions: [0]
|
|
14812
|
+
},
|
|
14813
|
+
// =========================================================
|
|
14814
|
+
// React Native Sinks
|
|
14815
|
+
// =========================================================
|
|
14816
|
+
{
|
|
14817
|
+
// WebView with user-controlled source
|
|
14818
|
+
method: "source",
|
|
14819
|
+
class: "WebView",
|
|
14820
|
+
type: "xss",
|
|
14821
|
+
cwe: "CWE-79",
|
|
14822
|
+
severity: "critical",
|
|
14823
|
+
argPositions: [0]
|
|
14824
|
+
// { html: userInput } or { uri: userInput }
|
|
14825
|
+
},
|
|
14826
|
+
{
|
|
14827
|
+
// Open arbitrary URLs
|
|
14828
|
+
method: "openURL",
|
|
14829
|
+
class: "Linking",
|
|
14830
|
+
type: "open_redirect",
|
|
14831
|
+
cwe: "CWE-601",
|
|
14832
|
+
severity: "high",
|
|
14833
|
+
argPositions: [0]
|
|
14834
|
+
},
|
|
14835
|
+
{
|
|
14836
|
+
method: "canOpenURL",
|
|
14837
|
+
class: "Linking",
|
|
14838
|
+
type: "ssrf",
|
|
14839
|
+
cwe: "CWE-918",
|
|
14840
|
+
severity: "medium",
|
|
14841
|
+
argPositions: [0]
|
|
14842
|
+
},
|
|
14843
|
+
{
|
|
14844
|
+
// Expo WebBrowser
|
|
14845
|
+
method: "openBrowserAsync",
|
|
14846
|
+
class: "WebBrowser",
|
|
14847
|
+
type: "open_redirect",
|
|
14848
|
+
cwe: "CWE-601",
|
|
14849
|
+
severity: "high",
|
|
14850
|
+
argPositions: [0]
|
|
14851
|
+
},
|
|
14852
|
+
{
|
|
14853
|
+
method: "openAuthSessionAsync",
|
|
14854
|
+
class: "WebBrowser",
|
|
14855
|
+
type: "open_redirect",
|
|
14856
|
+
cwe: "CWE-601",
|
|
14857
|
+
severity: "high",
|
|
14858
|
+
argPositions: [0]
|
|
14859
|
+
},
|
|
14860
|
+
// =========================================================
|
|
14861
|
+
// Next.js Sinks
|
|
14862
|
+
// =========================================================
|
|
14863
|
+
{
|
|
14864
|
+
// Server-side redirect
|
|
14865
|
+
method: "redirect",
|
|
14866
|
+
type: "open_redirect",
|
|
14867
|
+
cwe: "CWE-601",
|
|
14868
|
+
severity: "high",
|
|
14869
|
+
argPositions: [0]
|
|
14870
|
+
},
|
|
14871
|
+
{
|
|
14872
|
+
// Router push with user-controlled URL
|
|
14873
|
+
method: "push",
|
|
14874
|
+
class: "router",
|
|
14875
|
+
type: "open_redirect",
|
|
14876
|
+
cwe: "CWE-601",
|
|
14877
|
+
severity: "medium",
|
|
14878
|
+
argPositions: [0]
|
|
14879
|
+
},
|
|
14880
|
+
{
|
|
14881
|
+
method: "replace",
|
|
14882
|
+
class: "router",
|
|
14883
|
+
type: "open_redirect",
|
|
14884
|
+
cwe: "CWE-601",
|
|
14885
|
+
severity: "medium",
|
|
14886
|
+
argPositions: [0]
|
|
14887
|
+
},
|
|
14888
|
+
// =========================================================
|
|
14889
|
+
// React/General JS Security Sinks
|
|
14890
|
+
// =========================================================
|
|
14891
|
+
{
|
|
14892
|
+
// Dynamic component loading
|
|
14893
|
+
method: "createElement",
|
|
14894
|
+
class: "React",
|
|
14895
|
+
type: "code_injection",
|
|
14896
|
+
cwe: "CWE-94",
|
|
14897
|
+
severity: "high",
|
|
14898
|
+
argPositions: [0]
|
|
14899
|
+
// When first arg is user-controlled string
|
|
14900
|
+
},
|
|
14901
|
+
{
|
|
14902
|
+
// Importing user-controlled modules
|
|
14903
|
+
method: "import",
|
|
14904
|
+
type: "code_injection",
|
|
14905
|
+
cwe: "CWE-94",
|
|
14906
|
+
severity: "critical",
|
|
14907
|
+
argPositions: [0]
|
|
14908
|
+
},
|
|
14909
|
+
{
|
|
14910
|
+
method: "require",
|
|
14911
|
+
type: "code_injection",
|
|
14912
|
+
cwe: "CWE-94",
|
|
14913
|
+
severity: "critical",
|
|
14914
|
+
argPositions: [0]
|
|
14915
|
+
},
|
|
14916
|
+
// =========================================================
|
|
14917
|
+
// Data Exposure Sinks (React Native)
|
|
14918
|
+
// =========================================================
|
|
14919
|
+
{
|
|
14920
|
+
// Logging sensitive data
|
|
14921
|
+
method: "log",
|
|
14922
|
+
class: "console",
|
|
14923
|
+
type: "information_exposure",
|
|
14924
|
+
cwe: "CWE-532",
|
|
14925
|
+
severity: "low",
|
|
14926
|
+
argPositions: [0]
|
|
14927
|
+
},
|
|
14928
|
+
{
|
|
14929
|
+
// Storing sensitive data insecurely
|
|
14930
|
+
method: "setItem",
|
|
14931
|
+
class: "AsyncStorage",
|
|
14932
|
+
type: "insecure_storage",
|
|
14933
|
+
cwe: "CWE-922",
|
|
14934
|
+
severity: "medium",
|
|
14935
|
+
argPositions: [1]
|
|
14572
14936
|
}
|
|
14573
14937
|
];
|
|
14574
14938
|
}
|
|
@@ -15650,6 +16014,113 @@ function findGetterSources(types, instanceFieldTaint, sourceCode) {
|
|
|
15650
16014
|
}
|
|
15651
16015
|
return sources;
|
|
15652
16016
|
}
|
|
16017
|
+
var JS_DOM_XSS_SINKS = [
|
|
16018
|
+
{ pattern: /\.innerHTML\s*=/, type: "xss", cwe: "CWE-79", severity: "critical" },
|
|
16019
|
+
{ pattern: /\.outerHTML\s*=/, type: "xss", cwe: "CWE-79", severity: "critical" },
|
|
16020
|
+
{ pattern: /document\.write\s*\(/, type: "xss", cwe: "CWE-79", severity: "critical" },
|
|
16021
|
+
{ pattern: /document\.writeln\s*\(/, type: "xss", cwe: "CWE-79", severity: "critical" },
|
|
16022
|
+
{ pattern: /\.insertAdjacentHTML\s*\(/, type: "xss", cwe: "CWE-79", severity: "critical" },
|
|
16023
|
+
{ pattern: /\.src\s*=/, type: "xss", cwe: "CWE-79", severity: "high" },
|
|
16024
|
+
{ pattern: /\.href\s*=/, type: "xss", cwe: "CWE-79", severity: "high" }
|
|
16025
|
+
];
|
|
16026
|
+
var JS_TAINTED_PATTERNS = [
|
|
16027
|
+
{ pattern: /\breq\.query\b/, type: "http_param" },
|
|
16028
|
+
{ pattern: /\breq\.params\b/, type: "http_param" },
|
|
16029
|
+
{ pattern: /\breq\.body\b/, type: "http_body" },
|
|
16030
|
+
{ pattern: /\breq\.headers\b/, type: "http_header" },
|
|
16031
|
+
{ pattern: /\breq\.cookies\b/, type: "http_cookie" },
|
|
16032
|
+
{ pattern: /\breq\.url\b/, type: "http_path" },
|
|
16033
|
+
{ pattern: /\breq\.path\b/, type: "http_path" },
|
|
16034
|
+
{ pattern: /\breq\.originalUrl\b/, type: "http_path" },
|
|
16035
|
+
{ pattern: /\breq\.files?\b/, type: "file_input" },
|
|
16036
|
+
{ pattern: /\brequest\.query\b/, type: "http_param" },
|
|
16037
|
+
{ pattern: /\brequest\.params\b/, type: "http_param" },
|
|
16038
|
+
{ pattern: /\brequest\.body\b/, type: "http_body" },
|
|
16039
|
+
{ pattern: /\brequest\.headers\b/, type: "http_header" },
|
|
16040
|
+
{ pattern: /\bctx\.query\b/, type: "http_param" },
|
|
16041
|
+
{ pattern: /\bctx\.params\b/, type: "http_param" },
|
|
16042
|
+
{ pattern: /\bctx\.request\b/, type: "http_body" },
|
|
16043
|
+
{ pattern: /\bprocess\.env\b/, type: "env_input" },
|
|
16044
|
+
{ pattern: /\bprocess\.argv\b/, type: "io_input" },
|
|
16045
|
+
{ pattern: /\blocation\.search\b/, type: "http_param" },
|
|
16046
|
+
{ pattern: /\blocation\.hash\b/, type: "http_param" },
|
|
16047
|
+
{ pattern: /\blocation\.href\b/, type: "http_path" },
|
|
16048
|
+
{ pattern: /\bdocument\.getElementById\b/, type: "dom_input" },
|
|
16049
|
+
{ pattern: /\bdocument\.querySelector\b/, type: "dom_input" },
|
|
16050
|
+
{ pattern: /\.value\b/, type: "dom_input" }
|
|
16051
|
+
];
|
|
16052
|
+
function findJavaScriptAssignmentSources(sourceCode, language) {
|
|
16053
|
+
const sources = [];
|
|
16054
|
+
if (!["javascript", "typescript"].includes(language)) {
|
|
16055
|
+
return sources;
|
|
16056
|
+
}
|
|
16057
|
+
const lines = sourceCode.split("\n");
|
|
16058
|
+
for (let lineNum = 0; lineNum < lines.length; lineNum++) {
|
|
16059
|
+
const line = lines[lineNum];
|
|
16060
|
+
const lineNumber = lineNum + 1;
|
|
16061
|
+
const assignmentMatch = line.match(/(?:(?:var|let|const)\s+)?(\w+)\s*=\s*(.+)/);
|
|
16062
|
+
if (assignmentMatch) {
|
|
16063
|
+
const varName = assignmentMatch[1];
|
|
16064
|
+
const rhs = assignmentMatch[2];
|
|
16065
|
+
for (const { pattern, type } of JS_TAINTED_PATTERNS) {
|
|
16066
|
+
if (pattern.test(rhs)) {
|
|
16067
|
+
const alreadyExists = sources.some(
|
|
16068
|
+
(s) => s.line === lineNumber && s.type === type
|
|
16069
|
+
);
|
|
16070
|
+
if (!alreadyExists) {
|
|
16071
|
+
sources.push({
|
|
16072
|
+
type,
|
|
16073
|
+
location: `${varName} = ${rhs.trim().substring(0, 50)}${rhs.length > 50 ? "..." : ""}`,
|
|
16074
|
+
severity: "high",
|
|
16075
|
+
line: lineNumber,
|
|
16076
|
+
confidence: 1,
|
|
16077
|
+
variable: varName
|
|
16078
|
+
});
|
|
16079
|
+
}
|
|
16080
|
+
break;
|
|
16081
|
+
}
|
|
16082
|
+
}
|
|
16083
|
+
}
|
|
16084
|
+
}
|
|
16085
|
+
return sources;
|
|
16086
|
+
}
|
|
16087
|
+
function findJavaScriptDOMSinks(sourceCode, language) {
|
|
16088
|
+
const sinks = [];
|
|
16089
|
+
if (!["javascript", "typescript"].includes(language)) {
|
|
16090
|
+
return sinks;
|
|
16091
|
+
}
|
|
16092
|
+
const lines = sourceCode.split("\n");
|
|
16093
|
+
for (let lineNum = 0; lineNum < lines.length; lineNum++) {
|
|
16094
|
+
const line = lines[lineNum];
|
|
16095
|
+
const lineNumber = lineNum + 1;
|
|
16096
|
+
for (const { pattern, type, cwe, severity } of JS_DOM_XSS_SINKS) {
|
|
16097
|
+
if (pattern.test(line)) {
|
|
16098
|
+
let method = "innerHTML";
|
|
16099
|
+
if (line.includes(".outerHTML")) method = "outerHTML";
|
|
16100
|
+
else if (line.includes("document.write(")) method = "document.write";
|
|
16101
|
+
else if (line.includes("document.writeln(")) method = "document.writeln";
|
|
16102
|
+
else if (line.includes(".insertAdjacentHTML")) method = "insertAdjacentHTML";
|
|
16103
|
+
else if (line.includes(".src")) method = "src";
|
|
16104
|
+
else if (line.includes(".href")) method = "href";
|
|
16105
|
+
const alreadyExists = sinks.some(
|
|
16106
|
+
(s) => s.line === lineNumber && s.cwe === cwe
|
|
16107
|
+
);
|
|
16108
|
+
if (!alreadyExists) {
|
|
16109
|
+
sinks.push({
|
|
16110
|
+
type,
|
|
16111
|
+
cwe,
|
|
16112
|
+
severity,
|
|
16113
|
+
line: lineNumber,
|
|
16114
|
+
location: line.trim().substring(0, 80),
|
|
16115
|
+
method
|
|
16116
|
+
});
|
|
16117
|
+
}
|
|
16118
|
+
break;
|
|
16119
|
+
}
|
|
16120
|
+
}
|
|
16121
|
+
}
|
|
16122
|
+
return sinks;
|
|
16123
|
+
}
|
|
15653
16124
|
var initialized = false;
|
|
15654
16125
|
async function initAnalyzer(options = {}) {
|
|
15655
16126
|
if (initialized) return;
|
|
@@ -15802,11 +16273,30 @@ async function analyze(code, filePath, language, options = {}) {
|
|
|
15802
16273
|
const taint = analyzeTaint(calls, types, baseConfig);
|
|
15803
16274
|
const getterSources = findGetterSources(types, constPropResult.instanceFieldTaint, code);
|
|
15804
16275
|
taint.sources.push(...getterSources);
|
|
16276
|
+
const jsAssignmentSources = findJavaScriptAssignmentSources(code, language);
|
|
16277
|
+
taint.sources.push(...jsAssignmentSources);
|
|
16278
|
+
const jsDOMSinks = findJavaScriptDOMSinks(code, language);
|
|
16279
|
+
for (const domSink of jsDOMSinks) {
|
|
16280
|
+
const alreadyExists = taint.sinks.some(
|
|
16281
|
+
(s) => s.line === domSink.line && s.cwe === domSink.cwe
|
|
16282
|
+
);
|
|
16283
|
+
if (!alreadyExists) {
|
|
16284
|
+
taint.sinks.push({
|
|
16285
|
+
type: "xss",
|
|
16286
|
+
cwe: domSink.cwe,
|
|
16287
|
+
line: domSink.line,
|
|
16288
|
+
location: domSink.location,
|
|
16289
|
+
method: domSink.method,
|
|
16290
|
+
confidence: 1
|
|
16291
|
+
});
|
|
16292
|
+
}
|
|
16293
|
+
}
|
|
15805
16294
|
logger.debug("Initial taint analysis", {
|
|
15806
16295
|
sources: taint.sources.length,
|
|
15807
16296
|
sinks: taint.sinks.length,
|
|
15808
16297
|
sanitizers: taint.sanitizers?.length ?? 0,
|
|
15809
|
-
getterSources: getterSources.length
|
|
16298
|
+
getterSources: getterSources.length,
|
|
16299
|
+
jsDOMSinks: jsDOMSinks.length
|
|
15810
16300
|
});
|
|
15811
16301
|
taint.sinks = taint.sinks.filter((sink) => !constPropResult.unreachableLines.has(sink.line));
|
|
15812
16302
|
taint.sinks = filterCleanArraySinks(
|
|
@@ -15948,6 +16438,49 @@ async function analyze(code, filePath, language, options = {}) {
|
|
|
15948
16438
|
taint.sinks.push(sink);
|
|
15949
16439
|
}
|
|
15950
16440
|
}
|
|
16441
|
+
if (interProc.propagatedSinks.length > 0 && taint.sources.length > 0) {
|
|
16442
|
+
if (!taint.flows) {
|
|
16443
|
+
taint.flows = [];
|
|
16444
|
+
}
|
|
16445
|
+
const sanitizerMethodNames = /* @__PURE__ */ new Set();
|
|
16446
|
+
for (const san of taint.sanitizers ?? []) {
|
|
16447
|
+
if (san.type === "javadoc_sanitizer") {
|
|
16448
|
+
const match = san.method.match(/^(\w+)\(\)$/);
|
|
16449
|
+
if (match) sanitizerMethodNames.add(match[1]);
|
|
16450
|
+
else sanitizerMethodNames.add(san.method);
|
|
16451
|
+
}
|
|
16452
|
+
}
|
|
16453
|
+
for (const sink of interProc.propagatedSinks) {
|
|
16454
|
+
if (sink.type === "external_taint_escape") continue;
|
|
16455
|
+
for (const edge of interProc.callEdges) {
|
|
16456
|
+
if (!interProc.taintedMethods.has(edge.calleeMethod)) continue;
|
|
16457
|
+
const method = interProc.methodNodes.get(edge.calleeMethod);
|
|
16458
|
+
if (!method) continue;
|
|
16459
|
+
if (sink.line < method.startLine || sink.line > method.endLine) continue;
|
|
16460
|
+
if (sanitizerMethodNames.has(method.name)) continue;
|
|
16461
|
+
for (const source of taint.sources) {
|
|
16462
|
+
if (source.line > edge.callLine) continue;
|
|
16463
|
+
if (source.type === "interprocedural_param" && source.confidence < 0.6) continue;
|
|
16464
|
+
if (taint.flows.some((f) => f.source_line === source.line && f.sink_line === sink.line)) continue;
|
|
16465
|
+
taint.flows.push({
|
|
16466
|
+
source_line: source.line,
|
|
16467
|
+
sink_line: sink.line,
|
|
16468
|
+
source_type: source.type,
|
|
16469
|
+
sink_type: sink.type,
|
|
16470
|
+
path: [
|
|
16471
|
+
{ variable: source.location, line: source.line, type: "source" },
|
|
16472
|
+
{ variable: `call to ${method.name}()`, line: edge.callLine, type: "use" },
|
|
16473
|
+
{ variable: sink.location, line: sink.line, type: "sink" }
|
|
16474
|
+
],
|
|
16475
|
+
confidence: sink.confidence * source.confidence * 0.85,
|
|
16476
|
+
sanitized: false
|
|
16477
|
+
});
|
|
16478
|
+
break;
|
|
16479
|
+
}
|
|
16480
|
+
break;
|
|
16481
|
+
}
|
|
16482
|
+
}
|
|
16483
|
+
}
|
|
15951
16484
|
const taintBridges = findTaintBridges(interProc);
|
|
15952
16485
|
taint.interprocedural = {
|
|
15953
16486
|
tainted_methods: Array.from(interProc.taintedMethods),
|