Skip to content

fix(ci): least-privilege workflow permissions — clears 12 CodeQL Actions-hardening alerts - #1280

Merged
rubenvdlinde merged 3 commits into
developmentfrom
fix/codeql-workflow-permissions
Aug 19, 2026
Merged

fix(ci): least-privilege workflow permissions — clears 12 CodeQL Actions-hardening alerts#1280
rubenvdlinde merged 3 commits into
developmentfrom
fix/codeql-workflow-permissions

Conversation

@rubenvdlinde

@rubenvdlinde rubenvdlinde commented Aug 17, 2026

Copy link
Copy Markdown
Contributor

What this is, accurately

The CodeQL check-run on development is red with the title
"8 new alerts including 1 high severity security vulnerability". That title
overstates what is actually there. Measured distribution of the 12 open alerts
on refs/heads/development
:

where rule severity count
.github/workflows/*.yml actions/missing-workflow-permissions medium 12
tests/vitest/ js/bad-tag-filter high 1
production lib/ or src/ 0

This is Actions-hardening debt. It is not a security incident, and no
production code is involved.
The single "high severity security
vulnerability" in the title is a regex in a vitest helper (see below); it is not
reachable by any user and is not a sanitiser.

An absent permissions: block does not mean a job was compromised — it means
the job ran with the repository default grant instead of a stated one. This PR
states them.

The 12 workflow alerts

Ten of the twelve jobs only call a reusable workflow in ConductionNL/.github.
For those, the block is not a new grant: the callee's own job already
declares the same set, so the effective token is byte-for-byte what it is today.

workflow job(s) block why
release.yml unstable, beta, stable contents: write callee release already declares exactly this
release-beta / release-development / release-stable release contents: write cuts the tag, uploads the asset; App Store uses NEXTCLOUD_APPSTORE_TOKEN, not GITHUB_TOKEN
sync-to-beta sync contents: write, pull-requests: write callee runs gh pr create for the standing dev to beta PR
issue-triage triage issues: write, contents: read all three callee jobs declare exactly this
openspec-sync sync issues: write, contents: read callee sync
documentation deploy contents: write, packages: write union of the callee's build / deploy / image jobs
pull-request-lint-check lint-check contents: read local job: checkout + npm ci + npm run lint
spec-validation validate contents: read local job: checkout + npm ci + npm run check:specs

The trap this PR deliberately avoids

A caller block is a CEILING, not a grant. GitHub validates the called
workflow's declared job permissions against it — including for jobs an if:
will skip. Capping any of these at contents: read does not make the job run
with less; it makes the call fail to start, with zero jobs and no
annotations
— an outage quieter than the red it replaces.

packages: write on documentation is load-bearing at runtime as well: the
callee's build-image input is default: true and no caller overrides it, so
the image job really does run on a push to documentation and really does push
to GHCR. (An earlier revision of this PR's comments claimed that job "never
runs" — that was wrong and is corrected in the second commit.)

packages: read is deliberately omitted from the two local jobs: .npmrc
points at the public registry and package-lock.json contains zero
npm.pkg.github.com entries, so npm ci never authenticates to GitHub Packages.

The 1 test alert — not fixed here, dismissed with a reason

js/bad-tag-filter (high) at tests/vitest/editorModalSlotContract.spec.js:54:

const scriptMatch = source.match(/<script[^>]*>([\s\S]*?)<\/script>/)

CodeQL's finding is "this regular expression does not match upper case
<SCRIPT> tags"
— a real observation about a sanitiser. This is not a
sanitiser. It is a parser that reads a first-party .vue file off disk with
fs.readFileSync to extract the component's own <script> block for a contract
assertion. There is no untrusted input and no HTML being filtered for safety;
Vue SFCs require a lowercase <script> block to compile at all.

Dismissed as used in tests with the reasoning recorded on the alert. The rule
is left enabled
— no blanket suppression.

Verification

  • All 14 workflows parse (yaml.safe_load).
  • Job-level sweep of the tree: 0 jobs without a permissions: block at
    workflow or job level.
  • Positive control — the same sweep against the pre-change development
    tree reports 12, and they are the same 12 job names CodeQL flagged. The
    check can say NO.

…low jobs

Clears all 12 open CodeQL alerts on `development`. Every one is
`actions/missing-workflow-permissions`, severity MEDIUM, and every one is in
`.github/workflows/`. No production code is involved and nothing here is a
vulnerability that was exploitable — an absent `permissions:` block means the
job runs with the repository default rather than a stated grant.

Ten of the twelve jobs only CALL a reusable workflow in
ConductionNL/.github. For those the block is not a new grant: the callee's own
job already declares the same permissions, so the effective token is unchanged.

  release.yml (3 jobs) / release-beta / release-development / release-stable
      contents: write  — callee `release` already declares exactly this
  sync-to-beta        contents: write + pull-requests: write  — callee `create-pr`
  issue-triage        issues: write + contents: read  — all 3 callee jobs
  openspec-sync       issues: write + contents: read  — callee `sync`
  documentation       contents: write + packages: write  — UNION of the callee's
                      build / deploy / image jobs

The two local jobs (pull-request-lint-check, spec-validation) are
checkout + `npm ci` + a lint/validate script, so `contents: read`. `packages:
read` is deliberately omitted: .npmrc points at the public registry and
package-lock.json has zero npm.pkg.github.com entries.

A caller block is a CEILING, not a grant: GitHub validates the callee's declared
job permissions against it, including for jobs an `if:` will skip. Tightening
any of these to `read` would make the call fail to START with zero jobs rather
than run with less — an outage quieter than the red it replaces.

Verified: all 14 workflows parse, and a job-level sweep of the tree reports 0
jobs without a block, against 12 before the change — the same 12 CodeQL names.
…image defaults to true

The comment claimed `packages: write` was needed only because GitHub statically
validates a callee's declared job permissions, and that the `image` job "never
runs" here. That is wrong on the second half.

`build-image` in ConductionNL/.github/.github/workflows/documentation.yml is
`type: boolean, default: true`, and none of the callers pass it. So the `image`
job DOES run on a push to `documentation`, and it really does `docker buildx`
push to GHCR. `packages: write` is load-bearing at RUNTIME, not merely
statically — dropping it would 403 that push.

Comment only; the permissions block itself is unchanged and was already correct.
@github-actions

Copy link
Copy Markdown
Contributor

Quality Report — ConductionNL/openconnector @ 58d358f

Check PHP Vue Security License Tests
lint
phpcs
phpmd
psalm
phpstan
phpmetrics
eslint
stylelint
build
build
check-specs
test-l10n
format
composer ✅ 154/154
npm ✅ 534/534
app:check-code ⏭️
info.xml
REUSE
PHPUnit
Newman
Playwright
Hydra gates

Quality workflow — 2026-08-17 04:57 UTC

Download the full PDF report from the workflow artifacts.

@github-actions

Copy link
Copy Markdown
Contributor

Quality Report — ConductionNL/openconnector @ ab6f5d1

Check PHP Vue Security License Tests
lint
phpcs
phpmd
psalm
phpstan
phpmetrics
eslint
stylelint
build
build
check-specs
test-l10n
format
composer ✅ 154/154
npm ✅ 534/534
app:check-code ⏭️
info.xml
REUSE
PHPUnit
Newman
Playwright
Hydra gates

Quality workflow — 2026-08-17 05:19 UTC

Download the full PDF report from the workflow artifacts.

@github-actions

Copy link
Copy Markdown
Contributor

Quality Report — ConductionNL/openconnector @ bc6f6bf

Check PHP Vue Security License Tests
lint
phpcs
phpmd
psalm
phpstan
phpmetrics
eslint
stylelint
build
build
check-specs
test-l10n
format
composer ✅ 145/145
npm ✅ 534/534
app:check-code ⏭️
info.xml
REUSE
PHPUnit
Newman
Playwright
Hydra gates

Quality workflow — 2026-08-19 14:53 UTC

Download the full PDF report from the workflow artifacts.

@rubenvdlinde
rubenvdlinde merged commit 676b161 into development Aug 19, 2026
50 of 79 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant