From 939bd2a6145f422c4d869dafe00bcd7ad29b0796 Mon Sep 17 00:00:00 2001 From: Mariano Fuentes Date: Wed, 15 Jul 2026 12:47:27 -0400 Subject: [PATCH 1/2] fix(security): resolve open CodeQL alerts (XSS, sanitization, workflow perms) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - #108 (high, js/xss-through-dom): preview.tsx assigned user-typed input to iframe.src / anchor href, allowing javascript:/data: URIs to execute in the preview context. Gate every URL sink through an http(s)-only validator. - #95 (high, js/incomplete-sanitization): generate-task-types.ts escaped single quotes but not backslashes when embedding task fields in single-quoted TS string literals. Add a helper that escapes backslash-first and apply it to id/name/department/frequency. - #80/#81/#84/#85 (medium, actions/missing-workflow-permissions): the two Trigger.dev deploy + two DB-migration workflows had no permissions block. Add top-level 'permissions: contents: read' (they only check out to build/ deploy/migrate — no repo writes). Verified: app typecheck clean for preview.tsx; codegen script typechecks; all 4 workflow YAMLs valid. Co-Authored-By: Claude Fable 5 --- .../workflows/database-migrations-main.yml | 4 ++ .../workflows/database-migrations-release.yml | 4 ++ .../workflows/trigger-tasks-deploy-main.yml | 4 ++ .../trigger-tasks-deploy-release.yml | 3 + .../components/preview/preview.tsx | 62 ++++++++++++++----- .../scripts/generate-task-types.ts | 15 +++-- 6 files changed, 71 insertions(+), 21 deletions(-) diff --git a/.github/workflows/database-migrations-main.yml b/.github/workflows/database-migrations-main.yml index 3fd87ba7d3..87a710c345 100644 --- a/.github/workflows/database-migrations-main.yml +++ b/.github/workflows/database-migrations-main.yml @@ -4,6 +4,10 @@ on: branches: - main workflow_dispatch: # Allows manual triggering + +permissions: + contents: read # only needs to check out the repo to run migrations + jobs: migrate: name: Run Database Migrations diff --git a/.github/workflows/database-migrations-release.yml b/.github/workflows/database-migrations-release.yml index b64c4798d0..6191bf3086 100644 --- a/.github/workflows/database-migrations-release.yml +++ b/.github/workflows/database-migrations-release.yml @@ -4,6 +4,10 @@ on: branches: - release workflow_dispatch: # Allows manual triggering + +permissions: + contents: read # only needs to check out the repo to run migrations + jobs: migrate: name: Run Database Migrations diff --git a/.github/workflows/trigger-tasks-deploy-main.yml b/.github/workflows/trigger-tasks-deploy-main.yml index b86b1a4166..8a171da69b 100644 --- a/.github/workflows/trigger-tasks-deploy-main.yml +++ b/.github/workflows/trigger-tasks-deploy-main.yml @@ -3,6 +3,10 @@ on: push: branches: - main + +permissions: + contents: read # only needs to check out the repo to build & deploy + jobs: deploy: runs-on: warp-ubuntu-latest-arm64-4x diff --git a/.github/workflows/trigger-tasks-deploy-release.yml b/.github/workflows/trigger-tasks-deploy-release.yml index 2b066c359b..30c922bac7 100644 --- a/.github/workflows/trigger-tasks-deploy-release.yml +++ b/.github/workflows/trigger-tasks-deploy-release.yml @@ -5,6 +5,9 @@ on: branches: - release +permissions: + contents: read # only needs to check out the repo to build & deploy + jobs: deploy: runs-on: warp-ubuntu-latest-arm64-4x diff --git a/apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/[automationId]/components/preview/preview.tsx b/apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/[automationId]/components/preview/preview.tsx index 0f54f92a96..861a591f5f 100644 --- a/apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/[automationId]/components/preview/preview.tsx +++ b/apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/[automationId]/components/preview/preview.tsx @@ -13,6 +13,24 @@ interface Props { url?: string; } +/** + * Only allow http(s) URLs to reach an iframe `src` or anchor `href`. User-typed + * input flows into the preview iframe, so an unvalidated value like + * `javascript:...` or `data:text/html,...` would execute in the preview context + * (XSS). Returns the normalized href, or undefined if the value isn't http(s). + */ +function toSafeUrl(value: string | undefined | null): string | undefined { + if (!value) return undefined; + try { + const parsed = new URL(value); + return parsed.protocol === 'http:' || parsed.protocol === 'https:' + ? parsed.href + : undefined; + } catch { + return undefined; + } +} + export function Preview({ className, disabled, url }: Props) { const [currentUrl, setCurrentUrl] = useState(url); const [error, setError] = useState(null); @@ -21,35 +39,42 @@ export function Preview({ className, disabled, url }: Props) { const iframeRef = useRef(null); const loadStartTime = useRef(null); + // Sanitized view of currentUrl used for every DOM sink (iframe src, anchor href). + const safeUrl = toSafeUrl(currentUrl); + useEffect(() => { setCurrentUrl(url); setInputValue(url || ''); }, [url]); const refreshIframe = () => { - if (iframeRef.current && currentUrl) { + if (iframeRef.current && safeUrl) { setIsLoading(true); setError(null); loadStartTime.current = Date.now(); iframeRef.current.src = ''; setTimeout(() => { if (iframeRef.current) { - iframeRef.current.src = currentUrl; + iframeRef.current.src = safeUrl; } }, 10); } }; const loadNewUrl = () => { - if (iframeRef.current && inputValue) { - if (inputValue !== currentUrl) { - setIsLoading(true); - setError(null); - loadStartTime.current = Date.now(); - iframeRef.current.src = inputValue; - } else { - refreshIframe(); - } + if (!iframeRef.current || !inputValue) return; + const safeInput = toSafeUrl(inputValue); + if (!safeInput) { + setError('Enter a valid http(s) URL'); + return; + } + if (safeInput !== currentUrl) { + setIsLoading(true); + setError(null); + loadStartTime.current = Date.now(); + iframeRef.current.src = safeInput; + } else { + refreshIframe(); } }; @@ -67,7 +92,12 @@ export function Preview({ className, disabled, url }: Props) {
- +