Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .github/workflows/ci-frontend.yml
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,10 @@ jobs:
# we only care if the toolbar will increase a lot
minimum-change-threshold: 1000

- name: Check toolbar for CSP eval violations
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice

if: needs.changes.outputs.frontend == 'true'
run: pnpm --filter=@posthog/frontend check-toolbar-csp-eval

jest:
runs-on: depot-ubuntu-24.04
needs: changes
Expand Down
102 changes: 102 additions & 0 deletions frontend/bin/check-toolbar-csp-eval.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
import * as parser from "@babel/parser";
import traverse from "@babel/traverse";
import fs from "fs";
import path from "path";

// Parse dist/toolbar.js and check for anything that might trigger CSP script-src violations

// The threat model here is:
// * We're not trying to protect against an attacker hiding an eval like window["ev" + "al"]("...").
// * This is OK because any actual violations will be caught by the customer's actual CSP rules.
// * We are just trying to prevent false positives in the CSP report.
// * This script is just meant to protect against us accidentally adding evals, which wouldn't be maliciously hidden.


// Identifiers that become code when the *first* argument is a string
const UNSAFE_TIMERS = new Set(["setTimeout", "setInterval", "setImmediate", "execScript"]);

const FUNCTION_CTORS = new Set([
"Function",
"AsyncFunction",
"GeneratorFunction",
"AsyncGeneratorFunction",
]);

function main() {
const filePath = 'dist/toolbar.js';
const absPath = path.resolve(process.cwd(), filePath);
const source = fs.readFileSync(absPath, "utf-8");

const ast = parser.parse(source, {
sourceType: "unambiguous",
});

const evals = [];

traverse.default(ast, {
CallExpression(p) {
const callee = p.get("callee");

// eval(...)
if (callee.isIdentifier({name: "eval"})) {
if (p.node.loc) {
evals.push({
type: 'eval()',
start: p.node.loc.start
});
}
}

// window['eval'](...)
else if (
callee.isMemberExpression() &&
callee.node.computed &&
callee.get("object").isIdentifier({name: "window"}) &&
callee.get("property").isStringLiteral({value: "eval"})
) {
if (p.node.loc) {
evals.push({type: 'window["eval"]()', start: p.node.loc.start});
}
}

// === 3. setTimeout + other functions with a string argument
else if (
(callee.isIdentifier() && UNSAFE_TIMERS.has(callee.node.name)) ||
(callee.isMemberExpression() &&
!callee.node.computed &&
callee.get("object").isIdentifier({name: "window"}) &&
callee.get("property").isIdentifier() &&
UNSAFE_TIMERS.has(callee.get("property").node.name))
) {
const firstArg = p.get("arguments")[0];
if (firstArg?.isStringLiteral()) {
evals.push({type: `${callee.node.name}(string)`, start: p.node.loc});
}
}
},

NewExpression(p) {
const callee = p.get("callee");

// new Function(...) + other Function constructors
if (callee.isIdentifier() && FUNCTION_CTORS.has(callee.node.name)) {
evals.push({
type: `new ${callee.node.name}()`,
start: p.node.loc.start
});

}
},
});

if (evals.length > 0) {
evals.forEach(({type, start: {line, column}}) =>
console.log(`${type}: line ${line}:${column}`)
);

process.exit(1);
}
}


main()
7 changes: 5 additions & 2 deletions frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,8 @@
"lint:js": "eslint src ../cypress ../products",
"lint:css": "stylelint \"../(frontend|products)/**/*.{css,scss}\" --ignore-path=frontend/.stylelintignore",
"format": "pnpm lint:js --fix && pnpm lint:css --fix && pnpm prettier",
"visualize-toolbar-bundle": "pnpm exec esbuild-visualizer --metadata ./toolbar-esbuild-meta.json --filename=toolbar-esbuild-bundle-visualization.html"
"visualize-toolbar-bundle": "pnpm exec esbuild-visualizer --metadata ./toolbar-esbuild-meta.json --filename=toolbar-esbuild-bundle-visualization.html",
"check-toolbar-csp-eval": "node ./bin/check-toolbar-csp-eval.mjs"
},
"dependencies": {
"@babel/runtime": "^7.24.0",
Expand All @@ -53,7 +54,7 @@
"@floating-ui/react": "^0.26.9",
"@lottiefiles/react-lottie-player": "^3.4.7",
"@medv/finder": "^3.1.0",
"@microlink/react-json-view": "^1.21.3",
"@microlink/react-json-view": "^1.26.2",
"@microsoft/fetch-event-source": "^2.0.1",
"@monaco-editor/react": "4.6.0",
"@posthog/ee": "workspace:*",
Expand Down Expand Up @@ -196,6 +197,8 @@
"zxcvbn": "^4.4.2"
},
"devDependencies": {
"@babel/parser": "^7.24.0",
"@babel/traverse": "^7.24.0",
"@parcel/packager-ts": "2.13.3",
"@parcel/transformer-typescript-types": "2.13.3",
"@storybook/addon-actions": "^7.6.4",
Expand Down
Loading
Loading