fieldshield 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +101 -0
- package/LICENSE +21 -0
- package/README.md +1045 -0
- package/THREAT_MODEL.md +360 -0
- package/dist/assets/fieldshield.css +1 -0
- package/dist/assets/fieldshield.worker.js +1 -0
- package/dist/components/FieldShieldInput.d.ts +330 -0
- package/dist/fieldshield.js +790 -0
- package/dist/fieldshield.umd.cjs +4 -0
- package/dist/hooks/useFieldShield.d.ts +157 -0
- package/dist/hooks/useSecurityLog.d.ts +139 -0
- package/dist/index.d.cts +23 -0
- package/dist/index.d.ts +23 -0
- package/dist/patterns.d.ts +105 -0
- package/dist/utils/collectSecureValue.d.ts +102 -0
- package/package.json +107 -0
package/THREAT_MODEL.md
ADDED
|
@@ -0,0 +1,360 @@
|
|
|
1
|
+
# FieldShield Threat Model
|
|
2
|
+
|
|
3
|
+
**Version:** 1.0
|
|
4
|
+
**Last updated:** 2026
|
|
5
|
+
**Maintained by:** FieldShield maintainers
|
|
6
|
+
|
|
7
|
+
This document describes what FieldShield protects against, what it explicitly does not protect against, what assumptions it makes about the environment, and the residual risks that consuming applications must address independently.
|
|
8
|
+
|
|
9
|
+
This document is intended for security engineers, compliance officers, and auditors evaluating FieldShield for use in regulated environments including healthcare (HIPAA), financial services (PCI-DSS), and enterprise SaaS (SOC 2).
|
|
10
|
+
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
## Contents
|
|
14
|
+
|
|
15
|
+
- [Protected assets](#protected-assets)
|
|
16
|
+
- [Threats mitigated](#threats-mitigated)
|
|
17
|
+
- [Threats not mitigated](#threats-not-mitigated)
|
|
18
|
+
- [Environment assumptions](#environment-assumptions)
|
|
19
|
+
- [Architecture security properties](#architecture-security-properties)
|
|
20
|
+
- [Residual risks](#residual-risks)
|
|
21
|
+
- [Compliance mapping](#compliance-mapping)
|
|
22
|
+
- [Vulnerability disclosure](#vulnerability-disclosure)
|
|
23
|
+
|
|
24
|
+
---
|
|
25
|
+
|
|
26
|
+
## Protected assets
|
|
27
|
+
|
|
28
|
+
FieldShield protects the following data categories when entered into or pasted into a FieldShieldInput field:
|
|
29
|
+
|
|
30
|
+
**Personally Identifiable Information (PII)** — 6 built-in patterns
|
|
31
|
+
|
|
32
|
+
- US Social Security Numbers (SSN)
|
|
33
|
+
- Email addresses
|
|
34
|
+
- Phone numbers (US and international)
|
|
35
|
+
- Credit card numbers (Visa, Mastercard, Amex)
|
|
36
|
+
- Dates of birth
|
|
37
|
+
- US Tax IDs / EINs
|
|
38
|
+
|
|
39
|
+
**Healthcare identifiers** — 1 built-in pattern
|
|
40
|
+
|
|
41
|
+
- UK National Insurance Numbers (NIN) — equivalent of US SSN for UK residents
|
|
42
|
+
|
|
43
|
+
**Credentials and secrets** — 6 built-in patterns
|
|
44
|
+
|
|
45
|
+
- AI API keys (OpenAI, Anthropic, Google)
|
|
46
|
+
- AWS access keys (permanent and temporary)
|
|
47
|
+
- GitHub personal access tokens (all formats)
|
|
48
|
+
- Stripe API keys (secret, publishable, restricted)
|
|
49
|
+
- JSON Web Tokens (JWT)
|
|
50
|
+
- PEM private key blocks (RSA, EC, OpenSSH)
|
|
51
|
+
|
|
52
|
+
**Custom data types** defined by the consuming application via `customPatterns`.
|
|
53
|
+
|
|
54
|
+
**Opt-in protected assets** — available via `OPT_IN_PATTERNS`, not active by default
|
|
55
|
+
|
|
56
|
+
The following identifiers are available as opt-in patterns. They were excluded from the defaults because their regex structure produces high false positive rates in free-text and clinical note fields, which degrades usability without improving security. Enable them via `customPatterns` only on fields where the specific data type is the expected input.
|
|
57
|
+
|
|
58
|
+
- International Bank Account Numbers (IBAN) — `IBAN`
|
|
59
|
+
Pattern `[A-Z]{2}\d{2}...` matches laboratory accession numbers, specimen container IDs, reagent lot codes, and GS1 product codes common in healthcare contexts.
|
|
60
|
+
- US DEA Registration Numbers — `DEA_NUMBER`
|
|
61
|
+
Pattern `[A-Z]{2}\d{7}` matches pharmaceutical lot numbers, product batch codes, and laboratory reagent IDs common in clinical notes and pharmacy systems.
|
|
62
|
+
- Passport numbers (US, EU, India formats) — `PASSPORT_NUMBER`
|
|
63
|
+
Pattern `[A-Z]{1,2}[0-9]{6,9}` matches medication lot numbers, ICD-10 codes, and specimen IDs.
|
|
64
|
+
- US National Provider Identifiers (NPI) — `NPI_NUMBER`
|
|
65
|
+
Pattern `[12]\d{9}` matches any 10-digit number starting with 1 or 2; ubiquitous in free text.
|
|
66
|
+
- SWIFT / BIC codes — `SWIFT_BIC`
|
|
67
|
+
Pattern `[A-Z]{4}[A-Z]{2}[A-Z0-9]{2}` matches any 8-letter word; high false positive rate in clinical notes.
|
|
68
|
+
|
|
69
|
+
---
|
|
70
|
+
|
|
71
|
+
## Threats mitigated
|
|
72
|
+
|
|
73
|
+
### T1 — DOM-based value scraping
|
|
74
|
+
|
|
75
|
+
**Threat:** A browser extension, injected script, session recording tool (FullStory, LogRocket, Hotjar), or automated scraper reads `input.value` from the DOM to capture sensitive data entered by the user.
|
|
76
|
+
|
|
77
|
+
**Mitigation:** FieldShield writes only scrambled `x` characters to `input.value`. The DOM never contains the real value at any point during or after input. The real value is stored exclusively in Web Worker memory (`internalTruth`) which is isolated from the main thread and inaccessible via DOM APIs.
|
|
78
|
+
|
|
79
|
+
**Verification:** Confirmed by Playwright tests asserting that `input.value`, all element attributes, localStorage, sessionStorage, and cookies never contain typed sensitive data.
|
|
80
|
+
|
|
81
|
+
**Residual risk:** While the user is actively typing, the real value also exists in `realValueRef` on the main thread — see [Residual risks](#residual-risks).
|
|
82
|
+
|
|
83
|
+
---
|
|
84
|
+
|
|
85
|
+
### T2 — Clipboard exfiltration via copy
|
|
86
|
+
|
|
87
|
+
**Threat:** A user selects sensitive text and copies it to the clipboard, from which it may be pasted into an LLM chat interface, email, or unsecured application. A browser extension monitoring clipboard contents may also capture it.
|
|
88
|
+
|
|
89
|
+
**Mitigation:** FieldShield intercepts `copy` events and writes masked content (`█` characters) to the clipboard via `e.clipboardData.setData()`. The selection range is mapped from scrambled DOM coordinates to real value coordinates, so partial copies also produce accurately masked output. The clipboard never receives the real value.
|
|
90
|
+
|
|
91
|
+
The `onSensitiveCopyAttempt` callback fires with a `SensitiveClipboardEvent` payload containing the timestamp, field label, matched pattern names, masked preview, and event type — enabling real-time audit logging.
|
|
92
|
+
|
|
93
|
+
**Verification:** Confirmed by Playwright tests reading the real system clipboard after a copy operation and asserting it contains `█` characters and not the real value.
|
|
94
|
+
|
|
95
|
+
---
|
|
96
|
+
|
|
97
|
+
### T3 — Clipboard exfiltration via cut
|
|
98
|
+
|
|
99
|
+
**Threat:** Same as T2 but via a cut operation. Additionally, cut operations must correctly update the field state — the cut portion must be removed from both the DOM and worker memory, and subsequent keystrokes must not produce spurious characters.
|
|
100
|
+
|
|
101
|
+
**Mitigation:** FieldShield intercepts `cut` events identically to copy events — masked content goes to the clipboard. After the cut, the real value is spliced to remove the cut portion, the DOM is re-scrambled to match the new length, `processText` is called with the new value, and `setSelectionRange` restores the cursor via `requestAnimationFrame` so the DOM update has committed before cursor placement.
|
|
102
|
+
|
|
103
|
+
**Verification:** Playwright tests confirm that the DOM length shortens after a cut, the clipboard contains masked content, and typing after a full cut produces exactly one character — not a spurious extra character from DOM/ref desync.
|
|
104
|
+
|
|
105
|
+
---
|
|
106
|
+
|
|
107
|
+
### T4 — Sensitive data exposure via paste
|
|
108
|
+
|
|
109
|
+
**Threat:** A user pastes sensitive data from the clipboard into a field that should not accept it (e.g. a "reason for visit" free-text field that receives an SSN). The pasted data lands in the DOM and is captured by recording tools or read by extensions.
|
|
110
|
+
|
|
111
|
+
**Mitigation:** FieldShield intercepts paste events before the browser inserts clipboard content. Pasted text is scanned synchronously against all active patterns using the same pattern source strings the worker uses. The `onSensitivePaste` callback fires with the findings before the paste lands.
|
|
112
|
+
|
|
113
|
+
The consuming application can block the paste entirely by returning `false` from `onSensitivePaste` — the field reverts to its previous value and the clipboard content is discarded. The DOM is never updated with the sensitive content in the blocked case.
|
|
114
|
+
|
|
115
|
+
**Verification:** Unit tests confirm that `onSensitivePaste` returning `false` leaves the DOM unchanged, and that `onSensitivePaste` is not called for a paste blocked by `maxProcessLength` (preventing a misleading security event for reverted input).
|
|
116
|
+
|
|
117
|
+
---
|
|
118
|
+
|
|
119
|
+
### T5 — Accidental credential exposure in developer-facing inputs
|
|
120
|
+
|
|
121
|
+
**Threat:** A user pastes an API key, JWT, or private key into a support chat field, bug report form, or configuration panel. The credential is captured by session recording tools or stored in application logs.
|
|
122
|
+
|
|
123
|
+
**Mitigation:** FieldShield's credential patterns (`AI_API_KEY`, `AWS_ACCESS_KEY`, `GITHUB_TOKEN`, `STRIPE_KEY`, `JWT`, `PRIVATE_KEY_BLOCK`) detect and flag these values in any FieldShieldInput field. The clipboard interception (T2, T3) and paste detection (T4) apply equally to credential values.
|
|
124
|
+
|
|
125
|
+
---
|
|
126
|
+
|
|
127
|
+
### T6 — Denial of service via adversarial regex input
|
|
128
|
+
|
|
129
|
+
**Threat:** An attacker submits a very long, specially crafted string designed to cause catastrophic backtracking in one or more regex patterns, consuming all available CPU in the worker thread and causing the application to stop responding.
|
|
130
|
+
|
|
131
|
+
**Mitigation:** The `maxProcessLength` prop (default 100,000 characters) blocks any input that would exceed the limit before it reaches the worker. Input is blocked entirely — not truncated — because truncation would create a blind spot where sensitive data beyond the limit is never scanned. All processing runs in a Web Worker so even in the worst case the main thread UI remains responsive. Each pattern is individually wrapped in try/catch so a single misbehaving pattern cannot prevent other patterns from running.
|
|
132
|
+
|
|
133
|
+
**Residual risk:** The default limit of 100,000 characters may be insufficient for some attack scenarios. Applications with highly sensitive fields should consider lower limits via the `maxProcessLength` prop. Performance testing at the chosen limit is recommended before production deployment.
|
|
134
|
+
|
|
135
|
+
---
|
|
136
|
+
|
|
137
|
+
### T7 — Stale worker responses after unmount
|
|
138
|
+
|
|
139
|
+
**Threat:** A worker posts an UPDATE response in the same tick that `terminate()` is called during component unmount. The response arrives on the main thread after the component has unmounted, causing a state update on an unmounted component and potentially exposing stale sensitive data in React's component tree.
|
|
140
|
+
|
|
141
|
+
**Mitigation:** A `cancelled` boolean flag is declared inside the worker lifecycle effect. The cleanup function sets `cancelled = true` before calling `terminate()`. The `onmessage` handler checks this flag and discards any response that arrives after unmount. The flag uses closure-as-reference semantics — both the handler and the cleanup function share the same memory address, so the handler sees the updated value immediately.
|
|
142
|
+
|
|
143
|
+
---
|
|
144
|
+
|
|
145
|
+
### T8 — Worker initialization failure leaving field unprotected
|
|
146
|
+
|
|
147
|
+
**Threat:** The Web Worker fails to initialize — due to a strict CSP (`worker-src 'none'`), a sandboxed iframe context, or browser memory pressure — and the component renders without protection. The user continues to type, but no masking or pattern detection occurs. The form appears functional but sensitive data is unprotected.
|
|
148
|
+
|
|
149
|
+
**Mitigation:** Worker instantiation is wrapped in a try/catch. On failure, `workerFailed` is set to `true` and the component automatically falls back to `a11yMode` — a native `type="password"` input with browser-native masking. Pattern detection is suspended in this state but clipboard protection and the native masking layer remain active. A `console.error` fires so developers see the fallback in DevTools. The consuming app can wire `onWorkerError` to surface a warning to the user.
|
|
150
|
+
|
|
151
|
+
**Verification:** Playwright tests confirm that when the Worker constructor is blocked via `page.addInitScript`, all fields render `type=password` inputs and the scrambling overlay is absent.
|
|
152
|
+
|
|
153
|
+
---
|
|
154
|
+
|
|
155
|
+
### T9 — Malformed worker message manipulating UI state
|
|
156
|
+
|
|
157
|
+
**Threat:** A malicious or malformed message is delivered to the worker's `onmessage` handler with unexpected payload types — for example, `masked` as a number or `findings` as a string. This could corrupt the `masked` display string or the `findings` array, causing the UI to show incorrect security state.
|
|
158
|
+
|
|
159
|
+
**Mitigation:** The `onmessage` handler validates payload structure before setting state — `typeof e.data.masked === "string"` and `Array.isArray(e.data.findings)` are both checked. Messages that fail validation are silently discarded. Only the message type `"UPDATE"` triggers state updates — all other message types are ignored.
|
|
160
|
+
|
|
161
|
+
**Verification:** Unit tests confirm that UPDATE messages with non-string `masked` or non-array `findings` are discarded and state remains unchanged.
|
|
162
|
+
|
|
163
|
+
---
|
|
164
|
+
|
|
165
|
+
## Threats not mitigated
|
|
166
|
+
|
|
167
|
+
### N1 — Kernel-level and debugger access
|
|
168
|
+
|
|
169
|
+
FieldShield does not protect against an attacker with kernel-level access to the host machine, a JavaScript debugger attached to the browser process, or a compromised browser itself. An attacker at this privilege level can inspect any memory in the process, including Web Worker memory.
|
|
170
|
+
|
|
171
|
+
**Why not mitigated:** No client-side JavaScript library can provide protection against an attacker at the operating system or debugger level. This is a fundamental constraint of the web platform.
|
|
172
|
+
|
|
173
|
+
**Recommendation:** Ensure users access your application only from managed, trusted devices in high-sensitivity environments. Consider step-up authentication for sensitive operations.
|
|
174
|
+
|
|
175
|
+
---
|
|
176
|
+
|
|
177
|
+
### N2 — OS-level keyloggers
|
|
178
|
+
|
|
179
|
+
FieldShield does not protect against keyloggers operating at the operating system level, outside the browser process. These tools intercept keystrokes before the browser receives them.
|
|
180
|
+
|
|
181
|
+
**Why not mitigated:** Same fundamental constraint as N1. No browser-based protection applies below the browser process boundary.
|
|
182
|
+
|
|
183
|
+
**Recommendation:** Managed device policies, EDR solutions, and device attestation are the appropriate controls for this threat.
|
|
184
|
+
|
|
185
|
+
---
|
|
186
|
+
|
|
187
|
+
### N3 — Network interception
|
|
188
|
+
|
|
189
|
+
FieldShield does not protect data in transit. Once `getSecureValue()` returns the plaintext to the application and the application sends it to a backend, FieldShield has no involvement in transmission security.
|
|
190
|
+
|
|
191
|
+
**Why not mitigated:** Network security is out of scope for a client-side input protection library.
|
|
192
|
+
|
|
193
|
+
**Recommendation:** Use TLS 1.2 or higher for all API endpoints that receive sensitive data. Do not log raw request bodies containing sensitive fields on the server.
|
|
194
|
+
|
|
195
|
+
---
|
|
196
|
+
|
|
197
|
+
### N4 — Main thread access to realValueRef
|
|
198
|
+
|
|
199
|
+
While the user is actively typing, the real value exists in `realValueRef` on the main thread in addition to worker memory. This is required to reconstruct the real value character by character from DOM events — without it, editing would be impossible.
|
|
200
|
+
|
|
201
|
+
A compromised third-party JavaScript library loaded on the same page, a React DevTools fiber traversal in development mode, or a debugger attached to the browser can read this value.
|
|
202
|
+
|
|
203
|
+
**Why not mitigated:** JavaScript is single-threaded on the main thread. Any code running on the main thread in the same browsing context can, in principle, access memory allocated by other code in that context. Complete isolation is not achievable in a browser-based JavaScript environment.
|
|
204
|
+
|
|
205
|
+
**Mitigation in place:** The value is stored only in `realValueRef` (a React ref, not state) and is never serialized to any other location on the main thread during typing. At rest (when the user is not typing), the value exists only in worker memory.
|
|
206
|
+
|
|
207
|
+
**Recommendation:** Use Content Security Policy (CSP) to restrict which scripts can execute on your page. Audit all third-party JavaScript dependencies. Do not load analytics, marketing, or advertising scripts on pages containing FieldShieldInput fields.
|
|
208
|
+
|
|
209
|
+
---
|
|
210
|
+
|
|
211
|
+
### N5 — GET_TRUTH accessible to any main thread code
|
|
212
|
+
|
|
213
|
+
`getSecureValue()` opens a private `MessageChannel` and posts a `GET_TRUTH` message to the worker. Any code running on the main thread with access to the worker reference can call this method. A compromised third-party library loaded on the same page could call `getSecureValue()` on any mounted FieldShieldInput ref it can reach.
|
|
214
|
+
|
|
215
|
+
**Why not mitigated:** v1 does not implement authentication for `GET_TRUTH` calls. A one-time token mechanism is planned for v1.1.
|
|
216
|
+
|
|
217
|
+
**Recommendation:** Do not expose FieldShieldInput refs to third-party code. Keep refs scoped to the component or context that needs them. Audit third-party JavaScript loaded on pages containing sensitive fields.
|
|
218
|
+
|
|
219
|
+
---
|
|
220
|
+
|
|
221
|
+
### N6 — Unstructured PHI and context-dependent identifiers
|
|
222
|
+
|
|
223
|
+
FieldShield detects structured sensitive data with recognisable formats. It does not detect unstructured PHI such as patient names, street addresses, facility names, or free-text clinical descriptions.
|
|
224
|
+
|
|
225
|
+
These identifiers cannot be detected reliably with regex. `"John Smith"` as a patient name is structurally identical to `"John Smith"` as a company name. Named Entity Recognition (NER) with semantic context is required — not pattern matching.
|
|
226
|
+
|
|
227
|
+
Additionally, some identifiers detected by FieldShield are individually non-sensitive but become PHI in context. NPI numbers are publicly searchable via the CMS NPPES registry. SWIFT/BIC codes identify banks, not individuals. FieldShield detects both because they frequently appear alongside PHI in clinical and fintech form fields — the detection is a signal of likely combination sensitivity, not a guarantee.
|
|
228
|
+
|
|
229
|
+
**Why not mitigated:** Regex-based detection of free-text names and addresses produces an unacceptable false positive rate. A pattern broad enough to catch arbitrary names would flag ordinary prose constantly.
|
|
230
|
+
|
|
231
|
+
**Recommendation:** Implement server-side NER-based PHI detection for clinical notes fields. FieldShield provides defence-in-depth for structured identifiers — it is not a complete HIPAA de-identification solution. HIPAA's Safe Harbor method lists 18 identifier categories; FieldShield covers a subset.
|
|
232
|
+
|
|
233
|
+
---
|
|
234
|
+
|
|
235
|
+
### N7 — Cross-field sensitive data combination
|
|
236
|
+
|
|
237
|
+
FieldShield detects sensitive patterns within a single field. It does not detect that a first name in one field combined with an SSN in another field constitutes a HIPAA minimum necessary data set or a linkable record.
|
|
238
|
+
|
|
239
|
+
**Why not mitigated:** Cross-field awareness would require a shared context across independent field instances, which conflicts with the worker isolation architecture. `FieldShieldForm` with cross-field detection is planned for v2.0.
|
|
240
|
+
|
|
241
|
+
**Recommendation:** Implement combination detection at the application level using the `onChange` callback, which provides findings for each field without exposing the real value.
|
|
242
|
+
|
|
243
|
+
---
|
|
244
|
+
|
|
245
|
+
### N8 — Server-side exposure
|
|
246
|
+
|
|
247
|
+
FieldShield protects data on the client side during input. Once `collectSecureValues` or `getSecureValue` returns plaintext to the application and that data is sent to a backend, FieldShield has no involvement in server-side storage, logging, or access controls.
|
|
248
|
+
|
|
249
|
+
**Why not mitigated:** Server-side data protection is outside the scope of a client-side library.
|
|
250
|
+
|
|
251
|
+
**Recommendation:** Apply field-level encryption for sensitive data at rest. Use tokenization for payment card data. Implement appropriate access controls on APIs that receive sensitive fields.
|
|
252
|
+
|
|
253
|
+
---
|
|
254
|
+
|
|
255
|
+
### N9 — IME composition (v1)
|
|
256
|
+
|
|
257
|
+
Input Method Editors (CJK input, voice-to-text via browser) may not be correctly reconstructed in `realValueRef` during composition events. The real value stored in the worker may be incorrect for composed input.
|
|
258
|
+
|
|
259
|
+
**Why not mitigated:** IME composition support is planned for v1.1.
|
|
260
|
+
|
|
261
|
+
**Recommendation:** Do not deploy FieldShield for CJK language input in v1. Use `a11yMode` as a fallback if your user base requires IME input — in `a11yMode`, the browser handles value management directly.
|
|
262
|
+
|
|
263
|
+
---
|
|
264
|
+
|
|
265
|
+
## Environment assumptions
|
|
266
|
+
|
|
267
|
+
FieldShield's security properties depend on the following environment assumptions being true. If any assumption is violated, the protections described in [Threats mitigated](#threats-mitigated) may not hold.
|
|
268
|
+
|
|
269
|
+
**A1 — HTTPS is in use.** FieldShield does not verify this, but Web Workers are only available in secure contexts. Running FieldShield over HTTP in production is not supported and voids the isolation guarantee.
|
|
270
|
+
|
|
271
|
+
**A2 — The page loads over a trusted CDN or origin.** If the JavaScript bundle itself is compromised at the CDN or origin level, an attacker can modify FieldShield's source code before it reaches the browser. Subresource Integrity (SRI) hashes on script tags mitigate this.
|
|
272
|
+
|
|
273
|
+
**A3 — Third-party scripts on the page are trusted.** FieldShield cannot prevent a malicious third-party script from reading `realValueRef` on the main thread during typing. A strict Content Security Policy that whitelists only trusted script sources is the appropriate control.
|
|
274
|
+
|
|
275
|
+
**A4 — The browser is not compromised.** FieldShield assumes the browser correctly enforces the Web Worker isolation boundary. A compromised browser binary voids this assumption.
|
|
276
|
+
|
|
277
|
+
**A5 — The user's device is not compromised.** OS-level keyloggers and rootkits operate below the browser and cannot be addressed by any browser-based control.
|
|
278
|
+
|
|
279
|
+
**A6 — React DevTools are not enabled in production.** React DevTools can traverse the fiber tree and read ref values. FieldShield should not be deployed with React DevTools enabled in production builds.
|
|
280
|
+
|
|
281
|
+
**A7 — Content Security Policy restricts worker origins.** FieldShield's worker isolation guarantee is strongest when a `worker-src 'self'` CSP directive is in place. Without it, a tampered build pipeline or compromised CDN could theoretically substitute a malicious worker. The CSP directive is not required for the library to function but is strongly recommended for regulated environments.
|
|
282
|
+
|
|
283
|
+
**A8 — The worker source has not been tampered with.** FieldShield's no-network guarantee is verifiable by inspecting `fieldshield.worker.ts` — it has zero imports and zero network API calls. If your build pipeline produces the worker from this source without modification, the guarantee holds. Subresource Integrity (SRI) verification on the worker script provides additional assurance.
|
|
284
|
+
|
|
285
|
+
---
|
|
286
|
+
|
|
287
|
+
## Architecture security properties
|
|
288
|
+
|
|
289
|
+
### Web Worker isolation
|
|
290
|
+
|
|
291
|
+
The Web Worker thread is a dedicated worker — it has no shared memory with the main thread. Communication is exclusively via `postMessage` and `MessageChannel`. The structured clone algorithm used by `postMessage` cannot transfer arbitrary object references, preventing the main thread from holding a reference to worker memory.
|
|
292
|
+
|
|
293
|
+
### MessageChannel point-to-point delivery
|
|
294
|
+
|
|
295
|
+
`GET_TRUTH` responses travel via a `MessageChannel` port, not via the broadcast `postMessage` channel. Browser extensions that monitor `window.postMessage` events cannot intercept `MessageChannel` messages because they are delivered directly to the specific port, not broadcast to all listeners on the page.
|
|
296
|
+
|
|
297
|
+
`port1` is explicitly closed after the response is received to release the port and eliminate any residual attack surface from an open channel.
|
|
298
|
+
|
|
299
|
+
### No-network guarantee
|
|
300
|
+
|
|
301
|
+
The worker contains no calls to `fetch()`, `XMLHttpRequest`, `WebSocket`, `EventSource`, or `navigator.sendBeacon()`. It communicates exclusively via `postMessage`. This is verifiable by inspecting the worker source directly — the `@security NO NETWORK ACCESS` comment at the top of the file is a documented, auditor-facing assertion of this property.
|
|
302
|
+
|
|
303
|
+
Enforce this guarantee at the infrastructure level with `worker-src 'self'` in your Content Security Policy. See the [Content Security Policy](#content-security-policy) section of the README for a full recommended CSP configuration.
|
|
304
|
+
|
|
305
|
+
### Pattern source string design
|
|
306
|
+
|
|
307
|
+
Patterns are stored as plain regex source strings in `patterns.ts` and sent to the worker via the CONFIG message. This design means the worker has no static imports — it is entirely self-contained. The worker cannot be isolated from patterns by a bundler misconfiguration.
|
|
308
|
+
|
|
309
|
+
### No sensitive data in React state
|
|
310
|
+
|
|
311
|
+
The real value is stored in `realValueRef` (a React ref) rather than `useState`. State updates schedule re-renders and the state value is accessible through React's fiber tree. Ref values do not appear in React's state tree and do not trigger re-renders.
|
|
312
|
+
|
|
313
|
+
### Cancelled flag race guard
|
|
314
|
+
|
|
315
|
+
A `cancelled` boolean in the worker lifecycle effect prevents state updates from arriving after component unmount. This eliminates a class of race conditions where stale worker responses could update state in an unmounted component tree.
|
|
316
|
+
|
|
317
|
+
---
|
|
318
|
+
|
|
319
|
+
## Residual risks
|
|
320
|
+
|
|
321
|
+
| Risk | Severity | Mitigation in FieldShield | Recommended application control |
|
|
322
|
+
| ---------------------------------------------------- | -------- | ------------------------------------------ | ------------------------------------ |
|
|
323
|
+
| `realValueRef` readable on main thread during typing | Medium | Stored in ref, not state or DOM | Strict CSP, third-party script audit |
|
|
324
|
+
| `GET_TRUTH` callable by any main thread code | Medium | None in v1 | Scope refs, audit third-party JS |
|
|
325
|
+
| Worker init failure leaving field unprotected | Medium | Auto-fallback to a11yMode + console.error | Wire onWorkerError, surface warning |
|
|
326
|
+
| Malformed worker message corrupting UI state | Low | Payload validated before state update | None required |
|
|
327
|
+
| OS keylogger captures keystrokes | High | None — out of scope | Managed devices, EDR |
|
|
328
|
+
| Debugger access to worker memory | High | None — out of scope | Production build hardening |
|
|
329
|
+
| IME composition value reconstruction | Low | None in v1 | Use a11yMode for CJK input |
|
|
330
|
+
| Regex backtracking beyond maxProcessLength | Low | maxProcessLength blocks input | Set appropriate limit per field |
|
|
331
|
+
| Server-side exposure after submission | High | None — out of scope | Field-level encryption, tokenization |
|
|
332
|
+
| Third-party script on same page | Medium | None — out of scope | CSP script-src whitelist |
|
|
333
|
+
| Unstructured PHI (names, addresses) | High | None — regex cannot detect free-text names | Server-side NER-based PHI detection |
|
|
334
|
+
|
|
335
|
+
---
|
|
336
|
+
|
|
337
|
+
## Compliance mapping
|
|
338
|
+
|
|
339
|
+
| Control | Framework | FieldShield coverage |
|
|
340
|
+
| --------------------------------- | ------------------ | ------------------------------------------------------------------------------------------------- |
|
|
341
|
+
| Technical access controls on ePHI | HIPAA § 164.312(a) | Worker isolation prevents DOM access; NPI and DEA patterns detect healthcare identifiers in forms |
|
|
342
|
+
| Audit controls | HIPAA § 164.312(b) | `useSecurityLog` provides structured clipboard and submission audit trail |
|
|
343
|
+
| Transmission security | HIPAA § 164.312(e) | `MessageChannel` prevents broadcast interception — application must implement TLS |
|
|
344
|
+
| Protect stored cardholder data | PCI-DSS Req 3 | DOM never contains card numbers — application must implement server-side encryption |
|
|
345
|
+
| Protect web-facing applications | PCI-DSS Req 6.4 | Clipboard interception prevents browser-based skimming; SWIFT/BIC detection flags wire transfers |
|
|
346
|
+
| Logical access controls | SOC 2 CC6.1 | `getSecureValue()` is the only retrieval path — no DOM access to sensitive values |
|
|
347
|
+
| Availability | SOC 2 A1 | `maxProcessLength` prevents DoS; worker init fallback keeps field usable if worker unavailable |
|
|
348
|
+
| Change management | SOC 2 CC8.1 | All patterns versioned in `patterns.ts` — changes are tracked in git history and CHANGELOG |
|
|
349
|
+
|
|
350
|
+
---
|
|
351
|
+
|
|
352
|
+
## Vulnerability disclosure
|
|
353
|
+
|
|
354
|
+
If you discover a security vulnerability in FieldShield, please report it responsibly.
|
|
355
|
+
|
|
356
|
+
**Do not** open a public GitHub issue for security vulnerabilities.
|
|
357
|
+
|
|
358
|
+
**Do** email the maintainer directly with a description of the vulnerability, steps to reproduce, and your assessment of severity. We will acknowledge receipt within 48 hours and aim to release a fix within 14 days for critical issues.
|
|
359
|
+
|
|
360
|
+
Security researchers who report valid vulnerabilities will be credited in the release notes unless they request anonymity.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
:root{--fieldshield-font-family: inherit;--fieldshield-font-size: .9375rem;--fieldshield-line-height: 1.5;--fieldshield-padding-y: .5rem;--fieldshield-padding-x: .75rem;--fieldshield-gap: .375rem;--fieldshield-findings-gap: .375rem;--fieldshield-min-height: 2.5rem;--fieldshield-border-radius: .375rem;--fieldshield-tag-border-radius: .25rem;--fieldshield-bg: #ffffff;--fieldshield-border-color: #d1d5db;--fieldshield-border-color-focus: #3b82f6;--fieldshield-text-color: #111827;--fieldshield-placeholder-color: #9ca3af;--fieldshield-label-color: #374151;--fieldshield-caret-color: #111827;--fieldshield-unsafe-border-color: #f59e0b;--fieldshield-unsafe-bg: #fffbeb;--fieldshield-unsafe-focus-ring: #f59e0b;--fieldshield-warning-color: #92400e;--fieldshield-tag-bg: #fde68a;--fieldshield-tag-color: #78350f;--fieldshield-mask-color: #111827;--fieldshield-mask-blocked-color: #b45309;--fieldshield-focus-ring-width: 3px;--fieldshield-focus-ring-offset: 2px;--fieldshield-transition-duration: .15s;--fieldshield-transition-easing: ease-in-out}.fieldshield-container{position:relative;display:flex;flex-direction:column;gap:var(--fieldshield-gap);width:100%;font-family:var(--fieldshield-font-family);font-size:var(--fieldshield-font-size);line-height:var(--fieldshield-line-height)}.fieldshield-label{display:block;color:var(--fieldshield-label-color);font-weight:500;cursor:default;-webkit-user-select:none;user-select:none}.fieldshield-field-wrapper{position:relative;min-height:var(--fieldshield-min-height);border:1px solid var(--fieldshield-border-color);border-radius:var(--fieldshield-border-radius);background-color:var(--fieldshield-bg);transition:border-color var(--fieldshield-transition-duration) var(--fieldshield-transition-easing),background-color var(--fieldshield-transition-duration) var(--fieldshield-transition-easing),box-shadow var(--fieldshield-transition-duration) var(--fieldshield-transition-easing)}.fieldshield-mask-layer{position:absolute;inset:0;display:flex;align-items:center;padding:var(--fieldshield-padding-y) var(--fieldshield-padding-x);color:var(--fieldshield-mask-color);font-family:var(--fieldshield-font-family);font-size:var(--fieldshield-font-size);line-height:var(--fieldshield-line-height);white-space:pre-wrap;word-break:break-all;pointer-events:none;-webkit-user-select:none;user-select:none;overflow:hidden}.fieldshield-field-wrapper:has(textarea) .fieldshield-mask-layer{display:block;align-items:normal}.fieldshield-mask-layer .fieldshield-blocked{color:var(--fieldshield-mask-blocked-color)}.fieldshield-placeholder{color:var(--fieldshield-placeholder-color);font-style:normal}.fieldshield-mask-unsafe{background-color:var(--fieldshield-unsafe-bg)}.fieldshield-grow{visibility:hidden;display:block;width:100%;min-height:var(--fieldshield-min-height);padding:var(--fieldshield-padding-y) var(--fieldshield-padding-x);font-family:var(--fieldshield-font-family);font-size:var(--fieldshield-font-size);line-height:var(--fieldshield-line-height);white-space:pre-wrap;word-break:break-all;pointer-events:none;-webkit-user-select:none;user-select:none;box-sizing:border-box}.fieldshield-field-wrapper:has(textarea){overflow:visible}.fieldshield-real-input{position:absolute;inset:0;width:100%;height:100%;padding:var(--fieldshield-padding-y) var(--fieldshield-padding-x);font-family:var(--fieldshield-font-family);font-size:var(--fieldshield-font-size);line-height:var(--fieldshield-line-height);color:transparent;caret-color:var(--fieldshield-caret-color);background:transparent;border:none;border-radius:var(--fieldshield-border-radius);outline:none;resize:none;cursor:text}.fieldshield-real-input::selection{color:transparent;background-color:color-mix(in srgb,var(--fieldshield-border-color-focus) 30%,transparent)}.fieldshield-a11y-input{display:block;width:100%;min-height:var(--fieldshield-min-height);padding:var(--fieldshield-padding-y) var(--fieldshield-padding-x);font-family:var(--fieldshield-font-family);font-size:var(--fieldshield-font-size);line-height:var(--fieldshield-line-height);color:var(--fieldshield-text-color);background-color:var(--fieldshield-bg);border:1px solid var(--fieldshield-border-color);border-radius:var(--fieldshield-border-radius);outline:none;box-sizing:border-box;transition:border-color var(--fieldshield-transition-duration) var(--fieldshield-transition-easing),box-shadow var(--fieldshield-transition-duration) var(--fieldshield-transition-easing)}.fieldshield-a11y-input::placeholder{color:var(--fieldshield-placeholder-color)}.fieldshield-findings{display:flex;flex-wrap:wrap;align-items:center;gap:var(--fieldshield-findings-gap);min-height:0;font-size:.8125rem;line-height:1.4}.fieldshield-warning-icon{color:var(--fieldshield-warning-color);flex-shrink:0}.fieldshield-warning-text{color:var(--fieldshield-warning-color);font-weight:500}.fieldshield-tag{display:inline-flex;align-items:center;padding:.125rem .4375rem;background-color:var(--fieldshield-tag-bg);color:var(--fieldshield-tag-color);border-radius:var(--fieldshield-tag-border-radius);font-size:.75rem;font-weight:600;letter-spacing:.02em;white-space:nowrap}.fieldshield-sr-only:not(:focus):not(:active){position:absolute;width:1px;height:1px;margin:-1px;padding:0;overflow:hidden;clip:rect(0,0,0,0);clip-path:inset(50%);white-space:nowrap;border-width:0}.fieldshield-field-wrapper:has(input[aria-invalid=true]),.fieldshield-a11y-input[aria-invalid=true]{border-color:var(--fieldshield-unsafe-border-color);background-color:var(--fieldshield-unsafe-bg)}.fieldshield-field-wrapper:focus-within{border-color:var(--fieldshield-border-color-focus);box-shadow:0 0 0 var(--fieldshield-focus-ring-width) color-mix(in srgb,var(--fieldshield-border-color-focus) 25%,transparent);outline:none}.fieldshield-field-wrapper:has(input[aria-invalid=true]):focus-within{border-color:var(--fieldshield-unsafe-focus-ring);box-shadow:0 0 0 var(--fieldshield-focus-ring-width) color-mix(in srgb,var(--fieldshield-unsafe-focus-ring) 25%,transparent)}.fieldshield-a11y-input:focus{border-color:var(--fieldshield-border-color-focus);box-shadow:0 0 0 var(--fieldshield-focus-ring-width) color-mix(in srgb,var(--fieldshield-border-color-focus) 25%,transparent);outline:none}.fieldshield-a11y-input[aria-invalid=true]:focus{border-color:var(--fieldshield-unsafe-focus-ring);box-shadow:0 0 0 var(--fieldshield-focus-ring-width) color-mix(in srgb,var(--fieldshield-unsafe-focus-ring) 25%,transparent)}@media(prefers-color-scheme:dark){:root{--fieldshield-bg: #1f2937;--fieldshield-border-color: #4b5563;--fieldshield-border-color-focus: #60a5fa;--fieldshield-text-color: #f9fafb;--fieldshield-placeholder-color: #6b7280;--fieldshield-label-color: #d1d5db;--fieldshield-caret-color: #f9fafb;--fieldshield-mask-color: #f9fafb;--fieldshield-unsafe-border-color: #d97706;--fieldshield-unsafe-bg: #1c1508;--fieldshield-unsafe-focus-ring: #d97706;--fieldshield-warning-color: #fbbf24;--fieldshield-tag-bg: #451a03;--fieldshield-tag-color: #fde68a;--fieldshield-mask-blocked-color: #d97706}}@media(forced-colors:active){.fieldshield-field-wrapper,.fieldshield-a11y-input{border:2px solid ButtonText;forced-color-adjust:auto}.fieldshield-field-wrapper:focus-within,.fieldshield-a11y-input:focus{outline:3px solid Highlight;outline-offset:var(--fieldshield-focus-ring-offset);box-shadow:none}.fieldshield-real-input{caret-color:ButtonText}.fieldshield-tag{border:1px solid ButtonText;background-color:Canvas;color:ButtonText}.fieldshield-warning-icon,.fieldshield-warning-text{color:ButtonText}}@media(prefers-reduced-motion:reduce){.fieldshield-field-wrapper,.fieldshield-a11y-input{transition:none}}[data-disabled] .fieldshield-field-wrapper,[data-disabled] .fieldshield-a11y-input{opacity:.5;cursor:not-allowed;pointer-events:none}[data-disabled] .fieldshield-label{opacity:.5;cursor:not-allowed}[data-disabled] .fieldshield-mask-layer{cursor:not-allowed}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
(function(){"use strict";let l={},c={},n="";const i=(s,e)=>{const t={};for(const[a,o]of Object.entries(s))try{t[a]=new RegExp(o,"gi")}catch{console.warn(`[FieldShield] Skipping invalid ${e} pattern "${a}".`)}return t},d=s=>{let e=s;const t=[],a={...l,...c};for(const[o,r]of Object.entries(a))r.lastIndex=0,r.test(s)&&(t.push(o),r.lastIndex=0,e=e.replace(r,p=>"█".repeat(p.length)));return{masked:e,findings:[...new Set(t)]}};self.onmessage=s=>{const e=s.data;switch(e.type){case"CONFIG":{l=i(e.payload.defaultPatterns,"default"),c=i(e.payload.customPatterns,"custom");break}case"PROCESS":{n=e.payload.text;const{masked:t,findings:a}=d(n);self.postMessage({type:"UPDATE",masked:t,findings:a});break}case"GET_TRUTH":{const t=s.ports[0];t?t.postMessage({text:n}):console.warn("[FieldShield] GET_TRUTH received with no MessagePort — caller will time out. Pass port2 via the transfer array.");break}case"PURGE":{n="",self.postMessage({type:"PURGED"});break}default:{console.warn(`[FieldShield] Worker received unknown message type: "${e.type}"`);break}}}})();
|