core/quality/eslint/require-window-post-message-target-origin.eslint.rule.ts

core/quality/eslint/require-window-post-message-target-origin.eslint.rule.ts is a file in GovLab Quality. 89 lines of code and 16 definitions.

import type { Rule } from "eslint";
import { govlabMeta } from "#core/factories/eslint.factory";

const WINDOW_IDENTIFIERS = new Set(["window", "self", "parent", "top", "opener", "globalThis"]);
const WINDOW_MEMBER_PROPERTIES = new Set(["parent", "top", "opener", "self", "contentWindow"]);
const REQUIRED_ARGUMENT_COUNT = 2;

interface AstNode {
    type?: string;
    name?: string;
    value?: unknown;
    computed?: boolean;
    object?: AstNode;
    property?: AstNode;
    callee?: AstNode;
    arguments?: unknown[];
}

const isAstNode = function isAstNode(value: unknown): value is AstNode {
    return value !== null && typeof value === "object";
};

const asNode = function asNode(value: unknown): AstNode | null {
    return isAstNode(value) ? value : null;
};

const isFramesAccess = function isFramesAccess(node: AstNode): boolean {
    if (node.type !== "MemberExpression" || node.computed !== true) {
        return false;
    }
    const base = node.object;
    if (base?.type === "Identifier" && base.name === "frames") {
        return true;
    }
    return base?.type === "MemberExpression" && base.property?.name === "frames";
};

const isWindowReceiver = function isWindowReceiver(node: AstNode | null): boolean {
    if (node === null) {
        return false;
    }
    if (node.type === "Identifier") {
        return typeof node.name === "string" && WINDOW_IDENTIFIERS.has(node.name);
    }
    if (node.type === "MemberExpression" && node.computed !== true) {
        return typeof node.property?.name === "string" && WINDOW_MEMBER_PROPERTIES.has(node.property.name);
    }
    return isFramesAccess(node);
};

const isPostMessageOnWindow = function isPostMessageOnWindow(call: ReturnType<typeof asNode>): boolean {
    const callee = asNode(call?.callee ?? null);
    if (callee?.type !== "MemberExpression" || callee.property?.name !== "postMessage") {
        return false;
    }
    return isWindowReceiver(asNode(callee.object ?? null));
};

const checkPostMessage = function checkPostMessage(context: Rule.RuleContext, node: Rule.Node): void {
    const call = asNode(node);
    if (!isPostMessageOnWindow(call)) {
        return;
    }
    const args = call?.arguments ?? [];
    if (args.length < REQUIRED_ARGUMENT_COUNT) {
        context.report({ messageId: "missingTargetOrigin", node });
        return;
    }
    const targetOrigin = asNode(args[1]);
    if (targetOrigin?.type === "Literal" && targetOrigin.value === "*") {
        context.report({ messageId: "wildcardTargetOrigin", node });
    }
};

export default {
    create(context: Rule.RuleContext): Rule.RuleListener {
        return Object.fromEntries([
            [
                "CallExpression",
                (node: Rule.Node): void => {
                    checkPostMessage(context, node);
                },
            ],
        ]);
    },
    meta: govlabMeta({
        canonical: ["input-validation"],
        description:
            "A cross-window postMessage (window/self/parent/top/opener/globalThis/contentWindow/frames[]) declares an explicit targetOrigin as its second argument — omitting it broadcasts the message to whatever origin the target window holds, so it can leak to an unintended origin; a same-realm channel (MessagePort/Worker/BroadcastChannel) whose second argument is a transfer list carries no origin and is never flagged",
        messages: {
            missingTargetOrigin:
                "Cross-window postMessage with no explicit targetOrigin. Pass the exact expected origin as the second argument so the message cannot leak to an unintended origin.",
            wildcardTargetOrigin:
                'Cross-window postMessage with a wildcard "*" targetOrigin — this delivers the message to whatever origin the target window currently holds. Replace "*" with the exact expected origin.',
        },
        ruleId: "require_window_post_message_target_origin",
    }),
} satisfies Rule.RuleModule;