mirror of
https://github.com/morluto/rea
synced 2026-10-26 14:25:49 -04:00
241 lines
7.5 KiB
TypeScript
241 lines
7.5 KiB
TypeScript
import * as t from "@babel/types";
|
|
|
|
import type {
|
|
ElectronIpcDescriptor,
|
|
ElectronIpcFinding,
|
|
ElectronSenderValidationFinding,
|
|
} from "./electronStaticAnalysisTypes.js";
|
|
import {
|
|
boundedExpression,
|
|
electronStaticValue,
|
|
handlerKind,
|
|
} from "./electronStaticAnalysisValues.js";
|
|
import { addLocatedFinding } from "./javascriptStaticAnalysisFindings.js";
|
|
import {
|
|
argumentNode,
|
|
calleeName,
|
|
range,
|
|
semanticStaticPropertyName,
|
|
} from "./javascriptStaticAnalysisHelpers.js";
|
|
import type { JavaScriptFindingContext } from "./javascriptStaticAnalysisState.js";
|
|
|
|
/** Inspect Electron IPC operations and validation candidates. */
|
|
export const inspectElectronIpcNode = (
|
|
node: t.Node,
|
|
context: JavaScriptFindingContext,
|
|
): void => {
|
|
if (t.isCallExpression(node)) {
|
|
inspectIpcCall(node, context);
|
|
inspectValidationCall(node, context);
|
|
}
|
|
if (t.isBinaryExpression(node)) inspectValidationComparison(node, context);
|
|
};
|
|
|
|
const inspectIpcCall = (
|
|
node: t.CallExpression,
|
|
context: JavaScriptFindingContext,
|
|
): void => {
|
|
const name = calleeName(node.callee);
|
|
const descriptor = ipcDescriptor(name);
|
|
if (descriptor === undefined) return;
|
|
const channelNode = argumentNode(node.arguments[0]);
|
|
const channelValue = electronStaticValue(context.source, channelNode);
|
|
const channel =
|
|
channelValue.status === "literal" && typeof channelValue.value === "string"
|
|
? channelValue.value
|
|
: null;
|
|
const channelReference =
|
|
channel === null
|
|
? {
|
|
channel: null,
|
|
channel_expression:
|
|
channelValue.status === "dynamic"
|
|
? channelValue.expression
|
|
: boundedExpression(context.source, channelNode),
|
|
}
|
|
: { channel, channel_expression: null };
|
|
const handlerNode =
|
|
descriptor.mode === "listen" || descriptor.mode === "handle"
|
|
? argumentNode(node.arguments[1])
|
|
: undefined;
|
|
const findingState =
|
|
descriptor.mode === "send"
|
|
? {
|
|
...descriptor,
|
|
handler_kind: null,
|
|
handler_location: null,
|
|
}
|
|
: descriptor.mode === "invoke"
|
|
? {
|
|
...descriptor,
|
|
handler_kind: null,
|
|
handler_location: null,
|
|
}
|
|
: handlerNode === undefined
|
|
? {
|
|
...descriptor,
|
|
handler_kind: "missing" as const,
|
|
handler_location: null,
|
|
}
|
|
: {
|
|
...descriptor,
|
|
handler_kind: handlerKind(handlerNode),
|
|
handler_location: range(handlerNode),
|
|
};
|
|
if (channel === null) context.accumulator.unknownFindings += 1;
|
|
if (findingState.handler_kind === "missing")
|
|
context.accumulator.unknownFindings += 1;
|
|
const finding: ElectronIpcFinding = {
|
|
...findingState,
|
|
...channelReference,
|
|
module_key: null,
|
|
location: range(node),
|
|
};
|
|
addLocatedFinding(context, {
|
|
collection: context.accumulator.ipc,
|
|
key: `electron-ipc\0${descriptor.side}\0${descriptor.operation}\0${channelReference.channel ?? channelReference.channel_expression}`,
|
|
node,
|
|
value: finding,
|
|
});
|
|
};
|
|
|
|
const inspectValidationComparison = (
|
|
node: t.BinaryExpression,
|
|
context: JavaScriptFindingContext,
|
|
): void => {
|
|
if (!["==", "===", "!=", "!==", "in"].includes(node.operator)) return;
|
|
const left = senderSubject(node.left);
|
|
const right = senderSubject(node.right);
|
|
if (left === undefined && right === undefined) return;
|
|
const expected = left === undefined ? node.left : node.right;
|
|
addValidation(context, node, {
|
|
subject: left ?? right ?? "sender-frame",
|
|
mechanism: `comparison:${node.operator}`,
|
|
expected: electronStaticValue(context.source, expected),
|
|
});
|
|
};
|
|
|
|
const inspectValidationCall = (
|
|
node: t.CallExpression,
|
|
context: JavaScriptFindingContext,
|
|
): void => {
|
|
if (
|
|
!t.isMemberExpression(node.callee) &&
|
|
!t.isOptionalMemberExpression(node.callee)
|
|
)
|
|
return;
|
|
const method = semanticStaticPropertyName(
|
|
node.callee.property,
|
|
node.callee.computed,
|
|
);
|
|
if (!["startsWith", "endsWith", "includes"].includes(method)) return;
|
|
const subject = t.isNode(node.callee.object)
|
|
? senderSubject(node.callee.object)
|
|
: undefined;
|
|
if (subject === undefined) return;
|
|
addValidation(context, node, {
|
|
subject,
|
|
mechanism: `call:${method}`,
|
|
expected: electronStaticValue(
|
|
context.source,
|
|
argumentNode(node.arguments[0]),
|
|
),
|
|
});
|
|
};
|
|
|
|
const addValidation = (
|
|
context: JavaScriptFindingContext,
|
|
node: t.Node,
|
|
input: Pick<
|
|
ElectronSenderValidationFinding,
|
|
"subject" | "mechanism" | "expected"
|
|
>,
|
|
): void => {
|
|
if (input.expected.status === "dynamic")
|
|
context.accumulator.unknownFindings += 1;
|
|
const finding: ElectronSenderValidationFinding = {
|
|
...input,
|
|
enforcement: "unknown",
|
|
module_key: null,
|
|
location: range(node),
|
|
};
|
|
addLocatedFinding(context, {
|
|
collection: context.accumulator.senderValidations,
|
|
key: `electron-validation\0${input.subject}\0${input.mechanism}\0${input.expected.status === "literal" ? String(input.expected.value) : input.expected.expression}`,
|
|
node,
|
|
value: finding,
|
|
});
|
|
};
|
|
|
|
const ipcDescriptor = (name: string): ElectronIpcDescriptor | undefined => {
|
|
const renderer = suffixOperation(name, "ipcRenderer");
|
|
if (renderer !== undefined) {
|
|
switch (renderer) {
|
|
case "send":
|
|
return { side: "renderer", operation: "send", mode: "send" };
|
|
case "sendSync":
|
|
return { side: "renderer", operation: "send-sync", mode: "send" };
|
|
case "invoke":
|
|
return { side: "renderer", operation: "invoke", mode: "invoke" };
|
|
case "postMessage":
|
|
return {
|
|
side: "renderer",
|
|
operation: "post-message",
|
|
mode: "send",
|
|
};
|
|
case "sendToHost":
|
|
return {
|
|
side: "renderer",
|
|
operation: "send-to-host",
|
|
mode: "send",
|
|
};
|
|
case "on":
|
|
return { side: "renderer", operation: "on", mode: "listen" };
|
|
case "once":
|
|
return { side: "renderer", operation: "once", mode: "listen" };
|
|
}
|
|
}
|
|
const main = suffixOperation(name, "ipcMain");
|
|
switch (main) {
|
|
case "on":
|
|
return { side: "main", operation: "on", mode: "listen" };
|
|
case "once":
|
|
return { side: "main", operation: "once", mode: "listen" };
|
|
case "handle":
|
|
return { side: "main", operation: "handle", mode: "handle" };
|
|
case "handleOnce":
|
|
return { side: "main", operation: "handle-once", mode: "handle" };
|
|
default:
|
|
return undefined;
|
|
}
|
|
};
|
|
|
|
const suffixOperation = (
|
|
name: string,
|
|
object: "ipcRenderer" | "ipcMain",
|
|
): string | undefined => {
|
|
const marker = `${object}.`;
|
|
const index = name.lastIndexOf(marker);
|
|
if (index < 0) return undefined;
|
|
const prefix = name.slice(0, index);
|
|
if (prefix !== "" && !prefix.endsWith(".")) return undefined;
|
|
const operation = name.slice(index + marker.length);
|
|
return operation.includes(".") ? undefined : operation;
|
|
};
|
|
|
|
const senderSubject = (
|
|
node: t.Node,
|
|
): ElectronSenderValidationFinding["subject"] | undefined => {
|
|
const name = t.isCallExpression(node)
|
|
? calleeName(node.callee)
|
|
: calleeName(node);
|
|
if (/(?:^|\.)sender\.getURL$/u.test(name)) return "sender-url";
|
|
if (/(?:^|\.)senderFrame\.origin$/u.test(name)) return "sender-origin";
|
|
if (/(?:^|\.)senderFrame\.url$/u.test(name)) return "sender-url";
|
|
if (/(?:^|\.)senderFrame$/u.test(name)) return "sender-frame";
|
|
if (/(?:^|\.)sender\.id$/u.test(name)) return "sender-id";
|
|
if (/(?:^|\.)frameId$/u.test(name)) return "frame-id";
|
|
if (/(?:^|\.)processId$/u.test(name)) return "process-id";
|
|
return undefined;
|
|
};
|