From 7c2417f5a991a0cafe25d53ea823aafe765d9651 Mon Sep 17 00:00:00 2001 From: Pouzor Date: Tue, 9 Jun 2026 16:51:32 +0200 Subject: [PATCH] fix(security): resolve code scanning alerts Add least-privilege 'permissions: contents: read' to quality, security and docker-ci workflows (actions/missing-workflow-permissions). Harden markdown table cell escaping in exportMarkdown: escape backslash before pipe and collapse newlines so untrusted values can't break the table (js/incomplete-sanitization). Add regression tests. ha-relevant: maybe --- .github/workflows/docker-ci.yml | 3 +++ .github/workflows/quality.yml | 3 +++ .github/workflows/security.yml | 3 +++ .../utils/__tests__/exportMarkdown.test.ts | 27 +++++++++++++++++++ frontend/src/utils/exportMarkdown.ts | 9 ++++--- 5 files changed, 42 insertions(+), 3 deletions(-) diff --git a/.github/workflows/docker-ci.yml b/.github/workflows/docker-ci.yml index 227be16..dde50c0 100644 --- a/.github/workflows/docker-ci.yml +++ b/.github/workflows/docker-ci.yml @@ -6,6 +6,9 @@ on: pull_request: branches: [main] +permissions: + contents: read + jobs: smoke-and-integration: runs-on: ubuntu-latest diff --git a/.github/workflows/quality.yml b/.github/workflows/quality.yml index b6a2bd4..6c8ded2 100644 --- a/.github/workflows/quality.yml +++ b/.github/workflows/quality.yml @@ -6,6 +6,9 @@ on: pull_request: branches: [main] +permissions: + contents: read + jobs: lint-scripts: runs-on: ubuntu-latest diff --git a/.github/workflows/security.yml b/.github/workflows/security.yml index 043efac..3118aec 100644 --- a/.github/workflows/security.yml +++ b/.github/workflows/security.yml @@ -8,6 +8,9 @@ on: schedule: - cron: '0 9 * * 1' # Weekly on Monday +permissions: + contents: read + jobs: secrets-scan: runs-on: ubuntu-latest diff --git a/frontend/src/utils/__tests__/exportMarkdown.test.ts b/frontend/src/utils/__tests__/exportMarkdown.test.ts index 6da24f4..edd52a1 100644 --- a/frontend/src/utils/__tests__/exportMarkdown.test.ts +++ b/frontend/src/utils/__tests__/exportMarkdown.test.ts @@ -53,6 +53,33 @@ describe('generateMarkdownTable', () => { expect(md).toContain('A\\|B') }) + it('escapes backslashes before pipes so the escape char is not ambiguous', () => { + const nodes = [makeNode({ label: 'A\\|B' })] + const md = generateMarkdownTable(nodes) + // backslash doubled, then the literal pipe escaped + expect(md).toContain('A\\\\\\|B') + }) + + it('collapses newlines in cell values so they do not break the table', () => { + const nodes = [makeNode({ label: 'line1\nline2', hostname: 'a\r\nb' })] + const lines = generateMarkdownTable(nodes).split('\n') + // header + separator + exactly one data row (no extra line from the value) + expect(lines).toHaveLength(3) + expect(lines[2]).toContain('line1 line2') + expect(lines[2]).toContain('a b') + }) + + it('escapes pipe characters inside service names', () => { + const nodes = [makeNode({ + label: 'Server', + services: [{ port: 80, protocol: 'tcp', service_name: 'web|proxy' }], + })] + const lines = generateMarkdownTable(nodes).split('\n') + // header + separator + exactly one data row — the pipe must not add a column + expect(lines).toHaveLength(3) + expect(lines[2]).toContain('web\\|proxy') + }) + it('generates one row per non-groupRect node', () => { const nodes = [ makeNode({ type: 'server', label: 'A' }, '1'), diff --git a/frontend/src/utils/exportMarkdown.ts b/frontend/src/utils/exportMarkdown.ts index 4dfef69..7205b9f 100644 --- a/frontend/src/utils/exportMarkdown.ts +++ b/frontend/src/utils/exportMarkdown.ts @@ -5,8 +5,11 @@ const EMPTY = '—' function cell(v: string | null | undefined): string { if (!v) return EMPTY - // Escape pipe chars so they don't break the table - return v.replace(/\|/g, '\\|') + // Escape backslashes first, then pipes, and collapse newlines so they don't break the table + return v + .replace(/\\/g, '\\\\') + .replace(/\|/g, '\\|') + .replace(/\r?\n/g, ' ') } export function generateMarkdownTable(nodes: Node[]): string { @@ -27,7 +30,7 @@ export function generateMarkdownTable(nodes: Node[]): string { cell(d.ip), cell(d.hostname), cell(d.status), - services, + cell(services), ] })