Security fix: keep GM-only page content out of saved Foundry journals - #147
Merged
Merged
Conversation
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Abw8XBoqPMgbdjkmkuhCN2
keyxmakerx
marked this pull request as ready for review
October 3, 2026 17:40
…-rz71xm # Conflicts: # CLAUDE.md # lang/en.json
7 of 9 tasks
…-rz71xm # Conflicts: # CLAUDE.md # tools/test-html-sanitizer.mjs
9 of 10 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Requested by Key Maker · project thread
Fixes: none (follow-up to #141; the private tracker entry stays open until this is merged and released)
Security implication: security fix. GM-only text and pictures from Chronicle are no longer stored in Foundry journal pages at all, so players can't reach them, even on pages they own.
Consumer-verified:
PUT /api/v1/campaigns/:id/entities/:idanswers with the updated entity,entry_htmlincluded (internal/plugins/syncapi/api_handler.go,UpdateEntity→c.JSON(http.StatusOK, updated)); owner keys receive GM-only spans (internal/plugins/syncapi/egress_sanitize.go:107).Foundry compatibility: not run in a live Foundry. Uses Foundry's own secret-block markup and a document-wide MutationObserver, the same approach #138 uses for GM pictures (v12 to v14). Live check: #142.
Mockup: n/a. Players see nothing new; see the GM-facing changes listed below.
What this changes
Before (after #141): GM-only text sat inside Foundry secret blocks. Foundry hides those blocks only when it draws the page. Every client still receives the page's text, and a player who owns a page sees its secret blocks. A Chronicle "edit" grant makes players owners in Foundry.
After: a synced page saved in Foundry holds a placeholder ("GM-only text, kept in Chronicle") where Chronicle has GM-only text or a GM-only picture. The real content never goes into the world's data.
Why
#141 hid the text from players who don't own the page, but did not take it out of the data Foundry sends to every client. Holding only placeholders closes both remaining gaps at once.
How:
scripts/_gm-secrets.mjs:hideSecretsreplaces each secret block's content with a placeholder. The block's id is an HMAC-SHA-256 of its content under the GM's API key, so a player can't test a guess against it. The SHA-256 is pure JS, because Web Crypto isn't available on Foundry servers reached over plain HTTP.restoreSecretsputs the content back before a push.hasClearSecretsfinds GM-only content still sitting in a page.scripts/gm-secret-view.mjsfills placeholders on the GM's screen. It skips editors, keeps Foundry's own buttons, and never changes the saved page.scripts/journal-sync.mjs:_pullHtmlhandles every pull._entryForPushand_playerNotesForPushhandle every push._hidePushedSecretsruns after pushes. It skips a journal while a newer edit is still waiting to be sent._hideStoredSecretsruns on connect.Deviations and limits:
Test plan
node --test tools/test-*.mjs: 1243 pass, 0 fail.tools/test-gm-secrets.mjsadds placeholder tests:CHRONICLE_DIR=../Chronicle bench/run.sh journals: 14 pass, 0 fail, against Chronicle main. Two scenarios cover this change:gm-secrets: the saved page holds no GM-only text; a GM edit keeps it GM-only in Chronicle; a secret block typed in Foundry, and words typed after a placeholder's label, reach Chronicle GM-only and then leave the saved page.gm-secrets-old: a journal stored by an older version is hidden on the next connect.Tenet self-check
lang/en.json).ai.mdfile index and the CLAUDE.md conventions line updatedGenerated by Claude Code