Skip to content

Commit 10f372e

Browse files
shigechikaclaude
andauthored
docs(security): add SECURITY.md and SRI hashes for CDN scripts (#14)
* docs(security): add SECURITY.md and SRI hashes for CDN scripts Following the formal security review pass requested in #8. Findings: - Token handling, public-only invariant, workflow permissions, shell injection, repoSlug() coercion, and dashboard XSS surface all check out (most were already addressed by PRs #2, #11, #12). - One finding remained: docs/index.html loaded chart.js and chartjs-adapter-date-fns from cdn.jsdelivr.net without integrity attributes, so a CDN/NPM compromise could swap the file at the same URL. Added SHA-384 SRI hashes plus crossorigin="anonymous" to both <script> tags. SECURITY.md captures the trust model (who can read/write what), private-vulnerability-reporting URL, the public-only invariant and where it's enforced, workflow trust boundaries, the frontend supply chain story, the Protect main ruleset, and a checklist for fork users covering PAT scope / Pages visibility / private-repo safety / secret naming / branch protection portability / rename detection. Closes #8 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(security): correct SRI hashes for CDN scripts The integrity values in the previous commit were computed from a transient bad curl response — both differed from the bytes jsdelivr actually serves. Verified by hashing each file three times in a row (stable) and grep-checking docs/index.html. Without this fix the browser would have rejected both <script> tags on SRI mismatch, leaving the dashboard blank. Correct hashes: - chart.js@4.4.1 sha384-9nhczxUqK87bcKHh20fSQcTGD4qq5GhayNYSYWqwBkINBhOfQLg/P5HG5lF1urn4 - chartjs-adapter-date-fns@3.0.0 sha384-cVMg8E3QFwTvGCDuK+ET4PD341jF3W8nO1auiXfuZNQkzbUUiBGLsIQUE+b1mxws Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent a381d29 commit 10f372e

2 files changed

Lines changed: 101 additions & 2 deletions

File tree

SECURITY.md

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
# Security
2+
3+
This document describes the security posture of `github-insights` for both the upstream repository (`shigechika/github-insights`) and forks created via "Use this template".
4+
5+
## Reporting a vulnerability
6+
7+
If you believe you have found a security issue, please report it privately through GitHub's [private vulnerability reporting](https://github.com/shigechika/github-insights/security/advisories/new) instead of opening a public issue. Public reports are also accepted, but private reports are preferred for anything with non-trivial blast radius.
8+
9+
For forks, replace `shigechika/github-insights` with your own repository in the URL above.
10+
11+
## Trust model
12+
13+
### Who can write what
14+
15+
| Actor | Scope |
16+
|---|---|
17+
| Repository owner / collaborators | Push to any branch, modify workflows, settings, secrets |
18+
| `github-actions[bot]` (GITHUB_TOKEN) | Push to `main` (used by the daily cron to update `data/traffic.json` and `README.md`); bypasses the `Protect main` ruleset |
19+
| `GH_INSIGHTS_PAT` (Fine-grained PAT) | Read-only Administration on the owner's repos. Used to call `gh api` for `users/<owner>/repos` and `repos/<owner>/<repo>/traffic/{views,clones}`. Cannot push or modify any repository. |
20+
21+
### Who can read what
22+
23+
| Asset | Visibility |
24+
|---|---|
25+
| Source code, workflows, `data/traffic.json` | Public (this is a public repo) |
26+
| Live dashboard at `https://<owner>.github.io/<repo>/` | Public |
27+
| `GH_INSIGHTS_PAT` | Stored as a repository secret; never echoed by scripts; redacted from Actions logs by GitHub |
28+
| Aggregated traffic counts (views, clones, uniques) | Public via `data/traffic.json` once the workflow commits — fundamentally what this project publishes |
29+
30+
## Public-only invariant
31+
32+
This tool is designed to **never fetch, store, or publish traffic data for private repositories**, even when the PAT happens to have broader access.
33+
34+
The invariant is enforced in `scripts/collect.sh`:
35+
36+
- The list of repos in scope is built once via `gh api users/<owner>/repos?type=public` (`collect.sh:25`). This is the *only* gate; the `IMPORTANT` comment block above that line warns future contributors not to weaken it.
37+
- The data-collection loop iterates over that list (`collect.sh:54`), so traffic API calls only target public repos.
38+
- The rename reconciliation loop (`collect.sh:37–50`) skips probing any repo name that is still in the public list (no rename could have happened), and only honors a detected rename when the resolved new name is also in the public list — so historical data is never reorganized toward a private repo's name even if a private name ever sneaks into `traffic.json`.
39+
40+
The only residual touch on potentially-private repos is the rename probe `gh api repos/<owner>/<old_name>` for names that have left the public list (renamed-away, deleted, or made private). The PAT is read-only Administration, so this is defence-in-depth only — see #9 for the full discussion.
41+
42+
## Workflow trust boundaries
43+
44+
`.github/workflows/collect.yml`:
45+
46+
- Declares `permissions: contents: write` — the minimum needed for the bot's commit-and-push at the end. No other token-scoped permissions.
47+
- Uses two distinct credentials in separate steps:
48+
- `GH_INSIGHTS_PAT` (env: `GH_TOKEN`) — passed to `bash scripts/collect.sh` and `bash scripts/generate-charts.sh` for `gh api` calls. Read-only Administration.
49+
- `GITHUB_TOKEN` (implicit via `actions/checkout`) — used by the final `git push`. Scoped to `contents: write` for this run only.
50+
- All third-party Actions are pinned to a full commit SHA (currently `actions/checkout@de0fac2e... # v6.0.2` in both `collect.yml` and `lint.yml`). Dependabot watches for updates.
51+
- A `concurrency: collect-traffic` group prevents overlapping cron and manual runs from racing on `data/traffic.json`.
52+
53+
## Frontend supply chain
54+
55+
`docs/index.html` loads two scripts from `cdn.jsdelivr.net`:
56+
57+
- `chart.js@4.4.1`
58+
- `chartjs-adapter-date-fns@3.0.0`
59+
60+
Both `<script>` tags carry `integrity="sha384-..."` and `crossorigin="anonymous"` attributes. A jsdelivr or NPM compromise that swapped the file at the same URL would be rejected by the browser's Subresource Integrity check.
61+
62+
When upgrading either dependency, regenerate the hash:
63+
64+
```bash
65+
curl -sL https://cdn.jsdelivr.net/npm/chart.js@<new-version>/dist/chart.umd.min.js \
66+
| openssl dgst -sha384 -binary | openssl base64 -A
67+
```
68+
69+
and update both `src` and `integrity` together.
70+
71+
## Branch protection
72+
73+
`main` is protected by a Repository Ruleset (`Protect main`, id 15561427) with:
74+
75+
- `deletion` blocked — `main` cannot be deleted
76+
- `non_fast_forward` blocked — history rewrite (force push) blocked
77+
78+
Pull-request requirement was deferred because adding `github-actions[bot]` to the bypass list via the API on a personal repo failed validation. It can be added later via the Web UI; see issue #7 for the trail.
79+
80+
## Fork (template) users
81+
82+
When you create a fork via "Use this template", read these notes before going live:
83+
84+
1. **PAT scope**. The setup guide in `README.md` walks through creating a Fine-grained PAT with `Administration: Read-only`. Use **All repositories** or **Only select repositories** — the *Public repositories* preset cannot grant the Administration permission and the workflow will fail. The PAT cannot push or modify anything; it only reads metadata.
85+
2. **Private-repo safety**. Even if your PAT is scoped to "All repositories", `scripts/collect.sh` filters with `?type=public` and never collects traffic from your private repos. Verify by reading the `IMPORTANT` block in `collect.sh:20–24`.
86+
3. **Pages visibility**. Once enabled, `data/traffic.json` is public via `raw.githubusercontent.com`. The data is aggregate counts (views, clones, uniques) for your *own* public repos — this is fundamentally what the project publishes.
87+
4. **Secret name**. The workflow expects the secret to be named `GH_INSIGHTS_PAT`. If you rename it, update `.github/workflows/collect.yml` accordingly.
88+
5. **Branch protection**. The `Protect main` ruleset is per-repository and does not carry across forks. If you want the same protection, recreate it on your fork (UI: Settings → Rules → Rulesets, or via the API as documented in #7).
89+
6. **Rename detection**. If you rename a public repo, history is automatically merged into the new name on the next cron run via the GitHub API's 301 redirect (`scripts/collect.sh:27–50`). No manual intervention needed.
90+
91+
## What's intentionally not in scope
92+
93+
- **Authentication, sessions, user data** — none. The dashboard is read-only and ships no auth code.
94+
- **Server-side state** — none. All persistence lives in `data/traffic.json` in the repo itself; no backend.
95+
- **Third-party tracking, cookies, analytics on the dashboard** — none.

docs/index.html

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,12 @@
44
<meta charset="UTF-8">
55
<meta name="viewport" content="width=device-width, initial-scale=1.0">
66
<title>github-insights</title>
7-
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.1/dist/chart.umd.min.js"></script>
8-
<script src="https://cdn.jsdelivr.net/npm/chartjs-adapter-date-fns@3.0.0/dist/chartjs-adapter-date-fns.bundle.min.js"></script>
7+
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.1/dist/chart.umd.min.js"
8+
integrity="sha384-9nhczxUqK87bcKHh20fSQcTGD4qq5GhayNYSYWqwBkINBhOfQLg/P5HG5lF1urn4"
9+
crossorigin="anonymous"></script>
10+
<script src="https://cdn.jsdelivr.net/npm/chartjs-adapter-date-fns@3.0.0/dist/chartjs-adapter-date-fns.bundle.min.js"
11+
integrity="sha384-cVMg8E3QFwTvGCDuK+ET4PD341jF3W8nO1auiXfuZNQkzbUUiBGLsIQUE+b1mxws"
12+
crossorigin="anonymous"></script>
913
<style>
1014
:root {
1115
color-scheme: light dark;

0 commit comments

Comments
 (0)