Skip to content

Commit c8059a0

Browse files
committed
feat: form sharing security hardening — XSS fix, encrypted-only responses, SHA-256 rk validation, re-submit prevention, dynamic columns, form title in viewer, help demo
1 parent 461da56 commit c8059a0

20 files changed

Lines changed: 2579 additions & 24 deletions
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
# Form Sharing Security Hardening
2+
3+
## Overview
4+
Hardened the zero-knowledge dual-key form sharing system with critical security fixes, reliability improvements, and UX enhancements.
5+
6+
## Security Fixes
7+
- **XSS prevention**: Added `escapeHtml()` for all response table cell rendering in `form-engine.js`
8+
- **Encryption enforcement**: Removed insecure `storeResponsePlain()` fallback — all responses now AES-GCM encrypted only
9+
- **rk validation**: SHA-256 hash of response key (`rkHash`) stored in Firestore, validated on load. Invalid `rk` values hide the Responses viewer
10+
- **Read-only interceptor**: Exempted `.form-dg-submit` from the global click blocker so form submission works on creator links
11+
12+
## UX Improvements
13+
- **Dynamic columns**: Response viewer collects column keys from ALL responses (handles schema changes over time)
14+
- **Re-submit prevention**: Submit button disabled + text changes to "⏳ Submitting…" after click
15+
- **Form title in viewer**: Response viewer header shows form title (e.g., "📊 Feedback Survey — Responses 2")
16+
- **Help demo**: Form feature now has a dedicated demo recording (`32_form_sharing.webp`) in the Help system
17+
18+
## Files Modified
19+
- `js/form-engine.js` — XSS escaping, encrypted-only storage, dynamic columns, form title in viewer
20+
- `js/form-docgen.js` — Re-submit prevention
21+
- `js/cloud-share.js` — SHA-256 rkHash storage/validation, submit button exemption
22+
- `js/help-mode.js` — Form help entry with dedicated demo
23+
- `firestore.rules` — rkHash field support in share document schemas
24+
- `public/assets/demos/32_form_sharing.webp` — New demo recording
25+
26+
## Firestore Rules
27+
- Updated to allow optional `rkHash` field in both quick share and secure share schemas
28+
- Rules deployed to `mdview-share` project
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
# Dual-Key Form Sharing — Implementation Plan (Final)
2+
3+
## Goal
4+
5+
Secure form sharing with dual links, encrypted responses, and creator-only response viewing. When a `{{Form:}}` doc is shared, generate two links: a **creator link** (with `&rk=` for viewing responses) and a **respondent link** (`&m=fill`, preview-locked, fill-only).
6+
7+
## Architecture
8+
9+
```
10+
Creator writes {{Form:}} → clicks Share
11+
12+
createCompactShare() detects form → generates rk + SHA-256(rk) = rkHash
13+
14+
Stores in Firestore: { d, k, t, wt, rkHash }
15+
16+
Produces TWO links:
17+
Creator: #s=<id>&rk=<rkString> (rkHash validates ownership)
18+
Respondent: #s=<id>&m=fill (preview-locked, no rk)
19+
20+
On load: hash URL's rk → compare with stored rkHash → show/hide Responses button
21+
```
22+
23+
> [!NOTE]
24+
> `rk` is NOT stored in Firestore (only its SHA-256 hash `rkHash` is). This preserves zero-knowledge: even inspecting the Firestore doc doesn't reveal the key.
25+
26+
---
27+
28+
## Changes Made
29+
30+
### Security Fixes (Critical)
31+
32+
| # | Issue | File | Fix |
33+
|---|-------|------|-----|
34+
| 1 | XSS in response table | `form-engine.js` | Added `escapeHtml()` for all cell values |
35+
| 2 | Plain-text response fallback | `form-engine.js` | Removed `storeResponsePlain()` — encryption-only |
36+
| 3 | Unvalidated `rk` parameter | `cloud-share.js` | SHA-256 `rkHash` stored in Firestore, validated on load |
37+
| 4 | Submit blocked on creator view | `cloud-share.js` | Exempted `.form-dg-submit` from read-only interceptor |
38+
39+
### Reliability & UX Fixes
40+
41+
| # | Issue | File | Fix |
42+
|---|-------|------|-----|
43+
| 5 | Columns from first response only | `form-engine.js` | Collect column keys from ALL responses |
44+
| 6 | Stale comment | `form-engine.js` | Removed |
45+
| 7 | No re-submit prevention | `form-docgen.js` | Button disabled + "⏳ Submitting…" |
46+
| 8 | Missing form title in viewer | `form-engine.js` | Extracted from response data or DOM |
47+
| 9 | Help/demo missing for Forms | `help-mode.js` | Added `32_form_sharing.webp` demo |
48+
49+
---
50+
51+
## Files Modified
52+
53+
#### [MODIFY] [cloud-share.js](file:///Users/jyotibose/textagent.github.io/js/cloud-share.js)
54+
- `hashResponseKey()` — SHA-256 hash of rk using `crypto.subtle.digest`
55+
- `doQuickShare()` / `doSecureShare()` — detect forms, generate rk, pass rkHash
56+
- `createCompactShare()` — store `rkHash` in Firestore doc
57+
- `loadSharedMarkdown()` — validate URL's rk against stored rkHash
58+
- Read-only interceptor — exempt `.form-dg-submit` and `.form-dg-responses-btn`
59+
60+
#### [MODIFY] [form-engine.js](file:///Users/jyotibose/textagent.github.io/js/form-engine.js)
61+
- `escapeHtml()` — XSS prevention for response table
62+
- Removed `storeResponsePlain()` — encryption-only storage
63+
- Dynamic column collection from all responses
64+
- Form title in response viewer header
65+
66+
#### [MODIFY] [form-docgen.js](file:///Users/jyotibose/textagent.github.io/js/form-docgen.js)
67+
- Re-submit prevention (disabled button + status text)
68+
69+
#### [MODIFY] [firestore.rules](file:///Users/jyotibose/textagent.github.io/firestore.rules)
70+
- `rkHash` field allowed in share document schemas (both quick and secure share)
71+
72+
#### [MODIFY] [help-mode.js](file:///Users/jyotibose/textagent.github.io/js/help-mode.js)
73+
- Form help entry → dedicated `32_form_sharing.webp` demo
74+
75+
#### [NEW] [32_form_sharing.webp](file:///Users/jyotibose/textagent.github.io/public/assets/demos/32_form_sharing.webp)
76+
- Demo recording of full form sharing flow
77+
78+
---
79+
80+
## Firestore Rules (Deployed)
81+
82+
```
83+
rkHash — optional string field in share documents
84+
responses subcollection — world-writable (create), world-readable, immutable (no update)
85+
```
86+
87+
## Verification Results
88+
89+
| Test | Result |
90+
|------|--------|
91+
| Form creation + sharing | ✅ Dual links generated |
92+
| Respondent submission | ✅ Success message shown |
93+
| Re-submit prevention | ✅ Button disabled after click |
94+
| Creator response viewer | ✅ Form title + correct columns |
95+
| Valid rk → Responses button | ✅ Appears correctly |
96+
| Fake rk → rejected | ✅ No Responses button (new shares only) |
97+
| `npm run build` | ✅ Clean build |
98+
99+
> [!IMPORTANT]
100+
> Legacy shares (created before rules deployment) don't have `rkHash` stored. The code safely rejects `rk` for legacy docs. Only newly shared forms get full rk validation.

css/ai-docgen.css

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1837,6 +1837,17 @@
18371837
background: rgba(59, 130, 246, 0.15);
18381838
}
18391839

1840+
/* Tools toolbar buttons — compact like GET/POST */
1841+
.fmt-tools-btn {
1842+
color: #06b6d4 !important;
1843+
font-weight: 700;
1844+
font-size: 0.78em !important;
1845+
}
1846+
1847+
.fmt-tools-btn:hover {
1848+
background: rgba(6, 182, 212, 0.15);
1849+
}
1850+
18401851
/* API Tags Group Container */
18411852
.fmt-api-group {
18421853
display: inline-flex;
@@ -2440,7 +2451,8 @@
24402451

24412452
.fmt-coding-btn {
24422453
color: #818cf8 !important;
2443-
font-weight: 500;
2454+
font-weight: 700;
2455+
font-size: 0.78em !important;
24442456
gap: 4px;
24452457
}
24462458

@@ -2484,7 +2496,8 @@
24842496

24852497
.fmt-media-btn {
24862498
color: #f472b6 !important;
2487-
font-weight: 500;
2499+
font-weight: 700;
2500+
font-size: 0.78em !important;
24882501
}
24892502

24902503
.fmt-media-btn:hover {

css/draw-docgen.css

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -134,7 +134,8 @@
134134
/* Toolbar button */
135135
.fmt-draw-btn {
136136
color: #6965db !important;
137-
font-weight: 600;
137+
font-weight: 700;
138+
font-size: 0.78em !important;
138139
}
139140

140141
/* ─── AI Generate Button & Prompt ─── */

0 commit comments

Comments
 (0)