# B21 — GitHub Actions workflow: no `permissions:` block; fork PR has full GITHUB_TOKEN scope

Bug ref      : always.md B.21 ; pharo.md §7
Severity     : HIGH (fork PR can run any code with default workflow scope)
File         : .github/workflows/continuous-integration-workflow.yaml
Lines (HEAD) : 1-3 (workflow `on: [push, pull_request]` with no permissions)

## Problem

```yaml
name: Continuous integration
on: [push, pull_request]

jobs:
    build-unixes:
        ...
```

Without an explicit `permissions:` block, GitHub grants the
workflow the repository's default token permissions, which on a
public repo is typically read-write for `contents`, `pull-requests`,
`issues`, `checks`, `statuses`, …

A pull request from a fork can rewrite `scripts/runTests.sh`
(see B13) and run with that token. The token then has scope to push
to forks the actor owns, leave comments, mark checks, etc.

## Fix

Add a top-level `permissions:` block restricted to `contents: read`.
Add per-job permissions only when needed (e.g. for the upload
artifact step, which only needs the default scope and is already
provided by `actions/upload-artifact`).

```diff
diff --git a/.github/workflows/continuous-integration-workflow.yaml b/.github/workflows/continuous-integration-workflow.yaml
index b1633efa7..ddbcb490a 100644
--- a/.github/workflows/continuous-integration-workflow.yaml
+++ b/.github/workflows/continuous-integration-workflow.yaml
@@ -1,6 +1,11 @@
 name: Continuous integration
 on: [push, pull_request]
 
+# Lock the workflow token to read-only. Fork PRs cannot escalate
+# beyond this scope. Elevate per-job if any job genuinely needs more.
+permissions:
+  contents: read
+
 jobs:
     build-unixes:
         name: Build Unixes
```

## Test plan

- Open a PR from a fork to a test branch; confirm CI still runs
  (read-only operations succeed).
- Add a malicious line to `runTests.sh` in a fork PR that tries to
  `gh api -X POST repos/.../comments`; CI step fails with a 403
  because the token does not have write scope on issues/PRs.

## Risk notes

- If any job genuinely needs write scope (e.g. posting a coverage
  comment), elevate that **single job's** permissions with
  `jobs.<job>.permissions:` rather than reopening the whole
  workflow.
- This is defence-in-depth on top of B13 (the script-side hash
  pin). Apply both.
