Stop embedding the control token in every page (v0.9.10)

Closes TODO 2 of the security audit in docs/prs/2026-07-08-PR#000.

Every rendered page carried the live control token in a meta tag so that
gittally.js could send it, but no GET is authenticated — so `curl … |
grep gittally-control-token` handed the token to anyone, and read access
was effectively write access.

Reading stays fully public, which is a requirement rather than an
oversight: build states, logs and artifacts must be linkable from Gitea,
chats or tickets without a login. Only the distribution of the token
changed. The meta tag is gone; gittally.js keeps the token in
localStorage and asks for it once per browser, so knowing it requires
shell access to `.git/gittally/control-token` on the host. A token the
server rejects is dropped and asked for once more, so a rotated secret is
not a dead end. As a request header it stays inherently CSRF-safe.

The five branches of that flow (first use, reuse, stale token, cancelled
prompt, wrong token twice) were exercised against the real source with a
throwaway node harness; the UI test now asserts the token does not appear
in the rendered page. `docs/deployment.md` gained a "Control Token"
section on the public-read/token-protected-write split.

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
mhoennig
2026-08-11 08:00:09 +02:00
co-authored by Claude
parent ea0b67d331
commit a734d91918
9 changed files with 103 additions and 16 deletions
@@ -6,7 +6,6 @@
<title th:text="${#strings.isEmpty(repoName)} ? ${title} + ' — GitTally' : ${title} + ' — ' + ${repoName} + ' — GitTally'">GitTally</title>
<link rel="icon" href="/favicon.svg" type="image/svg+xml">
<link rel="stylesheet" href="/gittally.css">
<meta name="gittally-control-token" th:content="${controlToken}">
<meta name="gittally-gitea-repo-url" th:content="${giteaRepoUrl}">
</head>
<body>
@@ -7,6 +7,14 @@
<div th:replace="~{fragments :: nav(${view})}"></div>
<div class="panel release-notes">
<h2>v0.9.10 <span class="muted">— 2026-08-11</span></h2>
<ul>
<li>The control token is no longer embedded in the pages — reading them is unauthenticated,
so anyone could have picked it out of the HTML. Viewing stays public; the first click on
a restart/cancel/delete button asks for the token once (it is in
<code>.git/gittally/control-token</code> on the host) and keeps it in the browser.</li>
</ul>
<h2>v0.9.9 <span class="muted">— 2026-08-11</span></h2>
<ul>
<li><strong>Changed default:</strong> <code>server.bindAddress</code> is now <code>127.0.0.1</code>