Skip to content

Commit c05d3fc

Browse files
artizclaude
andcommitted
ci: least-privilege GITHUB_TOKEN; serve UI renders pictures from the response
Clears the open CodeQL code-scanning alerts on master. Workflow permissions (8 medium alerts, actions/missing-workflow-permissions): ci.yml and npm-publish.yml declared no `permissions:` block, so every job ran with the repository's default GITHUB_TOKEN scope. Both now default to `contents: read` at the top level, with the two jobs that need more keeping their existing job-level grants (ci's `publish` pushes the release commit and tag; npm-publish's `publish-cuda` uploads binaries to a GitHub release — npm publishing itself uses NPM_TOKEN, not the workflow token). deps-update.yml was not flagged but had the same shape inverted — write for both jobs where only `cargo-update` opens the PR — so the grant moves onto that job and the weekly `audit` job drops to read. DOM text reinterpreted as HTML (1 high, js/xss-through-dom): the serve UI streamed the conversion into the result panel and then re-read `out.textContent` to find embedded `data:image/...` URIs for the gallery, so DOM text flowed into `img.src`. The response is now accumulated in a variable and mirrored into the panel — the gallery renders from what we received, never from the DOM — which also drops a quadratic re-read of a multi-megabyte textContent per chunk. The span scanner additionally skips `svg+xml`: an SVG is markup, and the pipeline only ever emits PNG crops, so such a run in the output is not ours to render. Verified: docling-serve suites green, the UI script parses (node --check), and the span scanner keeps a PNG data URI while rejecting an SVG one. Signed-off-by: artiz <artem.kustikov@gmail.com> Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EY5KAiquN4YpVf2PXEQkVT
1 parent 3e23b49 commit c05d3fc

4 files changed

Lines changed: 36 additions & 6 deletions

File tree

.github/workflows/ci.yml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,12 @@ on:
1515
required: false
1616
default: ""
1717

18+
# Least privilege for GITHUB_TOKEN: every job here only reads the checkout.
19+
# The one exception — `publish`, which pushes the release commit and tag —
20+
# raises it to `contents: write` on the job itself.
21+
permissions:
22+
contents: read
23+
1824
# Cancel superseded PR runs, but never interrupt a master run (it may publish).
1925
concurrency:
2026
group: ci-${{ github.workflow }}-${{ github.ref }}

.github/workflows/deps-update.yml

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,9 @@ on:
1414
- cron: "17 4 * * 1" # Mondays 04:17 UTC
1515
workflow_dispatch:
1616

17+
# Least privilege by default; the `cargo-update` job raises what it needs.
1718
permissions:
18-
contents: write # push the deps/cargo-update branch
19-
pull-requests: write # open/refresh the update PR
19+
contents: read
2020

2121
jobs:
2222
# Weekly known-CVE scan of the committed lockfiles. Runs unconditionally
@@ -68,6 +68,9 @@ jobs:
6868
6969
cargo-update:
7070
runs-on: ubuntu-latest
71+
permissions:
72+
contents: write # push the deps/cargo-update branch
73+
pull-requests: write # open/refresh the update PR
7174
steps:
7275
- uses: actions/checkout@v7
7376
with:

.github/workflows/npm-publish.yml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,12 @@ on:
4242
type: boolean
4343
default: false
4444

45+
# Least privilege for GITHUB_TOKEN: publishing goes to npm with NPM_TOKEN, so
46+
# the token only needs the checkout. `publish-cuda`, which uploads binaries to
47+
# a GitHub release, raises it to `contents: write` on the job itself.
48+
permissions:
49+
contents: read
50+
4551
concurrency:
4652
group: npm-publish-${{ inputs.tag || github.ref }}
4753
cancel-in-progress: false

crates/docling-serve/src/index.html

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -170,7 +170,12 @@ <h2>Examples</h2>
170170
if (start === -1) break;
171171
const marker = text.indexOf(';base64,', start);
172172
const subtype = marker === -1 ? '' : text.slice(start + 11, marker);
173-
if (marker === -1 || subtype.length > 20 || !/^[a-z.+-]+$/.test(subtype)) {
173+
// `svg+xml` is excluded on purpose: an SVG is markup, and while a browser
174+
// does not run scripts in one loaded through <img src>, the pipeline never
175+
// emits SVG pictures anyway (crops are PNG) — so a data:image/svg+xml run
176+
// in the output is not ours to render.
177+
if (marker === -1 || subtype.length > 20 || !/^[a-z.+-]+$/.test(subtype)
178+
|| subtype === 'svg+xml') {
174179
from = start + 11;
175180
continue;
176181
}
@@ -263,18 +268,28 @@ <h2>Examples</h2>
263268
return;
264269
}
265270
// Stream the body into the panel as it arrives (Markdown streams page by page).
271+
// The text is accumulated in a variable and mirrored into the panel, never
272+
// read back out of it: re-reading a multi-megabyte textContent per chunk is
273+
// quadratic, and the gallery must render from the response we received
274+
// rather than from DOM text (CodeQL js/xss-through-dom).
266275
out.textContent = '';
276+
let body = '';
267277
const reader = response.body.getReader();
268278
const decoder = new TextDecoder();
269279
for (;;) {
270280
const { done, value } = await reader.read();
271281
if (done) break;
272-
out.textContent += decoder.decode(value, { stream: true });
282+
const chunk = decoder.decode(value, { stream: true });
283+
body += chunk;
284+
out.textContent += chunk;
273285
}
274286
if (to === 'json' || to === 'chunks') {
275-
try { out.textContent = JSON.stringify(JSON.parse(out.textContent), null, 2); } catch {}
287+
try {
288+
body = JSON.stringify(JSON.parse(body), null, 2);
289+
out.textContent = body;
290+
} catch {}
276291
}
277-
try { renderImages(out.textContent, to); } catch (e) { console.error('gallery:', e); }
292+
try { renderImages(body, to); } catch (e) { console.error('gallery:', e); }
278293
} catch (e) {
279294
out.classList.add('err');
280295
out.textContent = String(e);

0 commit comments

Comments
 (0)