Files
rea/src/domain/javascript/electronStaticAnalysisIpc.ts
T

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;
};