Repository navigation
Wizard persists its whole state (including a literal webhook URL) in localStorage under forgelm.wizard.state with no expiry; the privacy policy says only two values are stored (CR-A18-016) #367
Description
Wave 1 · theme D1-11: Other high-severity packages · highest severity High · 4 finding(s) · effort small
Summary
A user pastes a Slack webhook URL into the wizard on a shared machine. It stays in localStorage, and the privacy page says only language and theme are stored.
Primary finding
CR-A18-016 — Wizard persists its whole state (including a literal webhook URL) in localStorage under forgelm.wizard.state with no expiry; the privacy policy says only two values are stored
severity Medium · reachability default · intent contract-violation · confidence confirmed · independently confirmed · reproduced end to end · already tracked: KNOWN-OPEN DOCS-02/14/15/16 (S15) — docs/roadmap/phase-16-trust-surface-hardening.md:772,780-781
What happens. saveState writes the full state under STORAGE_KEY 'forgelm.wizard.state' on every input (wizard.js:67, 313-317, 2660-2663), including webhookUrl (a bearer secret when pasted literally), monitoring endpoint, provider/system names and risk text; it is cleared only by Reset (wizard.js:3001-3007). privacy.body in all six locales (translations.js:4345-4390) says only forgelm-lang and forgelm-theme are stored, and no guard compares site/js localStorage keys with the privacy text.
Where.
site/js/wizard.js:67(confirmed)site/js/wizard.js:314-317(saveState)site/js/wizard.js:807-814site/js/translations.js:4345,4354,4363,4372,4381,4390 (privacy.body in 6 locales)site/js/wizard.js:316site/js/wizard.js:2660-2662(webhook input listener)site/js/wizard.js:1170-1173site/js/wizard.js:2660-2663(confirmed)site/README.md:47-53site/guide.html:40
Evidence.
- node h/storage.js: saveState stores webhookUrl 'https://hooks.slack.com/services/T000/B000/XXXX' verbatim under forgelm.wizard.state.
- Code reading only; privacy text and wizard persistence both located and compared.
- main.js:133, i18n.js:26 and wizard.js:67 are the only localStorage keys in site/js. The privacy.body text enumerates only the first two. The webhook value is persisted via persist() in the urlInput 'input' listener (wizard.js:2660-2663).
Recommended fix. Add forgelm.wizard.state to the privacy text in all locales, and either avoid persisting a literal webhook URL or warn next to the field.
Other findings with the same root cause
These close with the same fix; update them in the same pull request.
CR-A18-037 — Contact-form promises ('not shared with third parties', 'not retained after the conversation is resolved', 'deletion: we delete the email thread') ignore the copy held by Formspree
severity Medium · reachability default · intent contract-violation · confidence confirmed · independently confirmed · not reproduced (code trace) · already tracked: KNOWN-OPEN DOCS-02/14/15/16 (S15 privacy) — docs/roadmap/phase-16-trust-surface-hardening.md:772
What happens. The form posts every field to a third-party endpoint: contact.html:136 sets action="https://formspree.io/f/xnjlejbb", and main.js:237-241 sends fetch(form.action, {method:'POST', body:new FormData(form)}), which includes name, email, organisation, topic, subject, message and _subject. The browser also sends its IP and User-Agent. The notice names Formspree as the conduit, but every locale then promises that the email is not shared with third parties and is not retained after the conversation is resolved, and that deletion 'is straightforward: we delete the email thread' (rights section). Those promises cover only the maintainer's mailbox. Whatever submission archive Formspree keeps on its side is not covered, and the notice offers no deletion or retention commitment for it. The notice never says that Formspree acts as a processor under a DPA, and that is the only reading under which 'not shared with third parties' would hold (a processor is excluded from 'third party' by Art. 4(10)). It also does not say that Formspree receives the visitor's IP and User-Agent, because it lists only 'the values you typed'. The form is also the only documented channel for security disclosures (contact.channel.security_body, translations.js:4162), so vulnerability details go into the same storage, with the same undisclosed retention. Whether and for how long Formspree keeps submissions is an external fact I could not verify offline (see research questions). The defect is that the notice's retention and deletion statement has no mechanism behind it for the processor copy.
Failure scenario. A visitor submits the form and later requests erasure. The maintainer deletes the mailbox thread as the notice describes, the visitor is told the data is gone, and the submission (with IP) may stay with Formspree under Formspree's own retention. The visitor relied on the notice's 'not retained' promise when deciding to submit.
Where.
site/contact.html:136site/js/main.js:207site/js/translations.js:4354site/js/translations.js:4141site/js/translations.js:4162site/js/main.js:236-240
Evidence.
- main.js:237
fetch(form.action, { method: 'POST', body: new FormData(form), headers: { Accept: 'application/json' } }); EN body: 'We do not add it to a mailing list, do not share it with third parties, and do not retain it after the conversation is resolved.' and 'deletion is straightforward: we delete the email thread.' The TR/DE/FR/ES/ZH bodies make the same promises.
Recommended fix. State that Formspree processes submissions as a processor (and cite the DPA if one is signed). Disclose that IP and User-Agent reach Formspree. Give a retention period that covers the Formspree copy, and add a step to delete the Formspree record to the erasure procedure (or turn off submission archiving in Formspree, if the plan allows it).
TECH-T16-003 — Site and browser stack: Formspree form endpoint and the site's contact-form privacy statements (site/contact.html:138, site/js/main.js:237, site/js/translations.js:4345) (incorrect-claim)
severity Next release
Item. Formspree form endpoint and the site's contact-form privacy statements (site/contact.html:138, site/js/main.js:237, site/js/translations.js:4345) · verdict: incorrect-claim · urgency: Next release
State in ForgeLM. site/contact.html:138 posts to https://formspree.io/f/xnjlejbb, and site/js/main.js:237 sends the form by fetch POST as FormData (name, email, topic, organization, subject, message). The form is the advertised security-disclosure channel: contact.html:159 and 220-221, translations.js:4162. The repo has no SECURITY.md (none at the root or under .github/). privacy.body (translations.js:4345 plus 5 mirrors) says the values are 'sent to Formspree, which forwards them to the project maintainer', defers to Formspree's policy, and says ForgeLM does 'not share it with third parties, and do[es] not retain it after the conversation is resolved' ('deletion is straightforward: we delete the email thread'). It also says only two localStorage values are stored, but site/js/wizard.js:67 and :316 persist a third key, forgelm.wizard.state, on quickstart.html. contact.form.intro (translations.js:4141) says 'no list, no tracking'.
State of the ecosystem on 2026-10-01. These were re-fetched today. Formspree's Privacy Policy is unchanged ('Last updated: April 24, 2022'). It collects names and emails plus IP address, browser type, domain names, access times and referring website, and 'may combine' them with partner data. It uses information 'in the United States'. Retention is 'as long as is necessary' or while the account is active, and log files are retained. Form owners are called 'independent controllers' under European law. The Security page says Formspree is hosted on AWS in the United States, uses AES-256 at rest, holds SOC 2 Type 2, and 'We rely on Standard Contractual Clauses (SCCs) as a data processor'. The subprocessor list has 22 entities, all in the US except Chartmogul (DE); among them are Amazon, Cloudflare, Google, Mailgun, OpenAI, Posthog, SendGrid and Sentry. The plans page gives a Submission Archive of 30 Days on Free and Unlimited on paid plans, and the help docs updated 2025-12-22 say the same. No public DPA exists: /legal/dpa, /legal/data-processing-addendum and /legal/data-processing-agreement all return 404. New today: the US Department of Commerce participant record shows Formspree, Inc. as an Inactive Participant. Its EU-U.S. Data Privacy Framework status is 'Inactive - Withdrawal' (originally certified 4/23/2019, inactive since 4/26/2022), so the DPF adequacy decision does not cover transfers to Formspree. They rest on SCCs alone. The Terms of Service were last updated 2022-12-20.
Impact. The privacy text is wrong or incomplete on what happens to contact-form data. Formspree keeps every submission in its US archive (30 days or unlimited, depending on the plan) and keeps logs with IP and browser metadata. Its transfer basis is SCCs, not the DPF. No public Art. 28 DPA exists, and Formspree's own documents call itself both 'processor' and the form owner an 'independent controller'. 'We delete the email thread' therefore does not delete the processor-held copy. The localStorage list also omits the wizard state key. Security-vulnerability reports go to the same US third-party archive (a design-risk under TM-v1, family 11), and the repo has no SECURITY.md alternative.
Recommendation. Rewrite the contact-form and storage sections of privacy.body in all 6 locales. State that Formspree (US, AWS) stores submissions and technical metadata, the archive retention of the plan actually used, and the transfer basis (SCCs). Add the forgelm.wizard.state localStorage key. Either purge the Formspree archive when a conversation closes and say so, or drop 'do not retain'. Obtain and record Formspree's DPA/SCC terms by request. Move security disclosures to GitHub private vulnerability reporting with a SECURITY.md, and tell reporters not to send exploit details through the form. Consider mailto or a self-hosted endpoint if a no-third-party stance is the goal.
Sources.
- Formspree Privacy Policy (Last updated: April 24, 2022) (2022-04-24)
- Formspree Security page (AWS US hosting, SCCs as data processor, SOC 2 Type 2) (undated (footer ©2026))
- Formspree Plans (Submission Archive: 30 Days free, Unlimited paid) (undated (retrieved 2026-10-01))
- Formspree Subprocessors (22 entities) (undated)
- Formspree Terms of Service (Last updated: December 20, 2022) (2022-12-20)
- Formspree Docs - Account limits (30 days of submission history on Free) (2025-12-22)
DOC-D4B09-014 — site/js/translations.js: false claim — 'Nothing [is collected]. No cookies are set. Two values are stored in localStorage on your device' (forgelm-lang, forgelm-theme)
severity High · already tracked: KNOWN-OPEN DOCS-03 docs/roadmap/phase-16-trust-surface-hardening.md:771-772,780-781
Where. site/js/translations.js:4345 (privacy.body, 'From normal browsing') · type: false claim
The document says. 'Nothing [is collected]. No cookies are set. Two values are stored in localStorage on your device' (forgelm-lang, forgelm-theme)
What the code does. A third localStorage key, forgelm.wizard.state, is written by the in-browser wizard on every input change (site/js/wizard.js:67,316; loaded by quickstart.html). It holds everything typed into the wizard: model, dataset paths, governance text, provider and system names, webhook and monitoring URLs. It persists until the user resets the wizard or clears site data.
Code references. site/js/wizard.js:67, site/js/wizard.js:310-320, site/js/wizard.js:1247
User impact. The privacy policy under-states what is stored. Values that can include webhook URLs (secrets) and organisation data sit in the browser indefinitely, without disclosure. The data never leaves the device, but the policy is inaccurate.
Proposed correction. Change to 'Three values are stored in localStorage on your device', and add: 'forgelm.wizard.state: your in-progress answers in the quickstart wizard (model, dataset, compliance fields, any webhook URL you type), kept so a refresh does not lose progress. Use Reset wizard or clear site data to remove it.' Also advise not to type secrets into the wizard.
Acceptance criteria
- A regression test reproduces the failure scenario and asserts the corrected behaviour.
- Documentation listed above is corrected in English and in its Turkish mirror in the same PR.
- Every finding in this issue is resolved or explicitly closed with a reason.
Found by the October 2026 ForgeLM code and documentation review against commit cab2947. Code links point at that commit; line numbers may have moved since. Finding IDs (CR-…, DOC-…, TECH-…) are stable references for this backlog.
Metadata
Metadata
Assignees
Labels
Type
Projects
- StatusShow more project fieldsTodo
Activity