From ef899b0802e65bc59a83eda8f5d551033f9293cd Mon Sep 17 00:00:00 2001 From: root Date: Thu, 6 Aug 2026 17:37:07 +0000 Subject: [PATCH] fix(security): resolve code scanning alerts - workflows: stop interpolating workflow_run.head_branch into shell run steps; use head_sha via env and gate on same-repo runs (actions/code-injection, actions/untrusted-checkout) - battle tests: use execFileSync with an argument array instead of a shell command string (js/shell-command-injection-from-environment) - edit.ts: escape all ffmpeg filtergraph metacharacters in applyLUT (js/incomplete-sanitization) - screenshots.ts: reword screenshot-filename pattern to avoid nested quantifiers (js/polynomial-redos) --- .github/workflows/docs.yml | 6 ++++-- .github/workflows/jsr.yml | 6 ++++-- .github/workflows/publish.yml | 6 ++++-- battle.test.ts | 4 ++-- deno-tests/battle.test.ts | 4 ++-- lib/helpers/edit.ts | 2 +- lib/helpers/screenshots.ts | 2 +- 7 files changed, 18 insertions(+), 12 deletions(-) diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 413d798..b07088e 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -27,8 +27,10 @@ jobs: - uses: actions/checkout@v5 - name: Checkout release tag - if: github.event_name == 'workflow_run' - run: git fetch origin "${{ github.event.workflow_run.head_branch }}" && git checkout FETCH_HEAD + if: github.event_name == 'workflow_run' && github.event.workflow_run.head_repository.full_name == github.repository + env: + HEAD_SHA: ${{ github.event.workflow_run.head_sha }} + run: git fetch origin "$HEAD_SHA" && git checkout FETCH_HEAD - name: Setup Node.js 20 uses: actions/setup-node@v5 diff --git a/.github/workflows/jsr.yml b/.github/workflows/jsr.yml index 8cebc07..276e8c4 100644 --- a/.github/workflows/jsr.yml +++ b/.github/workflows/jsr.yml @@ -22,8 +22,10 @@ jobs: - uses: actions/checkout@v5 - name: Checkout release tag - if: github.event_name == 'workflow_run' - run: git fetch origin "${{ github.event.workflow_run.head_branch }}" && git checkout FETCH_HEAD + if: github.event_name == 'workflow_run' && github.event.workflow_run.head_repository.full_name == github.repository + env: + HEAD_SHA: ${{ github.event.workflow_run.head_sha }} + run: git fetch origin "$HEAD_SHA" && git checkout FETCH_HEAD - uses: denoland/setup-deno@v2 with: diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 2348ef4..6123e50 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -25,8 +25,10 @@ jobs: - uses: actions/checkout@v5 - name: Checkout release tag - if: github.event_name == 'workflow_run' - run: git fetch origin "${{ github.event.workflow_run.head_branch }}" && git checkout FETCH_HEAD + if: github.event_name == 'workflow_run' && github.event.workflow_run.head_repository.full_name == github.repository + env: + HEAD_SHA: ${{ github.event.workflow_run.head_sha }} + run: git fetch origin "$HEAD_SHA" && git checkout FETCH_HEAD - name: Setup Node.js 20 uses: actions/setup-node@v5 diff --git a/battle.test.ts b/battle.test.ts index 8e84281..64e039b 100644 --- a/battle.test.ts +++ b/battle.test.ts @@ -8,7 +8,7 @@ import fs from 'node:fs'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; -import { execSync } from 'node:child_process'; +import { execFileSync } from 'node:child_process'; const __dirname = path.dirname(fileURLToPath(import.meta.url)); const TMP = path.join(__dirname, 'tmp'); @@ -49,7 +49,7 @@ function section(title) { section('SETUP — generating test media'); function ffmpegExec(args) { - execSync(`ffmpeg -y ${args}`, { stdio: 'pipe' }); + execFileSync('ffmpeg', ['-y', ...args.trim().split(/\s+/)], { stdio: 'pipe' }); } await run('generate input.mp4 (10s 640x360 video+audio)', () => { diff --git a/deno-tests/battle.test.ts b/deno-tests/battle.test.ts index 5a944d0..4f8ac83 100644 --- a/deno-tests/battle.test.ts +++ b/deno-tests/battle.test.ts @@ -9,7 +9,7 @@ import fs from 'node:fs'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; -import { execSync, execFileSync } from 'node:child_process'; +import { execFileSync } from 'node:child_process'; const __dirname = path.dirname(fileURLToPath(import.meta.url)); const TMP = path.join(__dirname, 'tmp_battle'); @@ -40,7 +40,7 @@ function section(title: string): void { } function ffmpegExec(args: string): void { - execSync(`ffmpeg -y ${args}`, { stdio: 'pipe' }); + execFileSync('ffmpeg', ['-y', ...args.trim().split(/\s+/)], { stdio: 'pipe' }); } diff --git a/lib/helpers/edit.ts b/lib/helpers/edit.ts index f00f18a..4ffc8dc 100644 --- a/lib/helpers/edit.ts +++ b/lib/helpers/edit.ts @@ -608,7 +608,7 @@ export async function applyLUT(opts: ApplyLutOptions): Promise { const isWindows = process.platform === 'win32'; const sanitised = isWindows ? lut : lut.replace(/\\/g, '/'); - const escapedLut = sanitised.replace(/:/g, '\\:').replace(/'/g, "\\'"); + const escapedLut = sanitised.replace(/(['\\,;\[\]%:])/g, '\\$1'); const filter = `lut3d='${escapedLut}':interp=${interp}`; const builder = new FFmpegBuilder(input).setBinary(binary) diff --git a/lib/helpers/screenshots.ts b/lib/helpers/screenshots.ts index 6696031..d5250e9 100644 --- a/lib/helpers/screenshots.ts +++ b/lib/helpers/screenshots.ts @@ -56,7 +56,7 @@ async function executeTimestamps(opts: ScreenshotOptions, timestamps: (string | const ts = timestamps[i]!; const ext = path.extname(filename) || '.png'; const base = path.basename(filename, ext); - const outName = base.replace(/%\d*\.?\d*[diouxX]/g, String(i + 1).padStart(4, '0')) + ext; + const outName = base.replace(/%\d*(?:\.\d*)?[diouxX]/g, String(i + 1).padStart(4, '0')) + ext; const outPath = path.join(folder, outName); const args: string[] = ['-y', '-ss', String(toSeconds(ts)), '-i', input, '-vframes', '1'];