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/README.md
ADDED
|
@@ -0,0 +1,1045 @@
|
|
|
1
|
+
# FieldShield
|
|
2
|
+
|
|
3
|
+
Sensitive input protection for React applications. Prevents DOM-based exposure of typed values, intercepts clipboard operations, and provides structured security logging for HIPAA and PCI-DSS compliance.
|
|
4
|
+
|
|
5
|
+
```tsx
|
|
6
|
+
<FieldShieldInput
|
|
7
|
+
label="Social Security Number"
|
|
8
|
+
onSensitiveCopyAttempt={(e) => log(e)}
|
|
9
|
+
onSensitivePaste={(e) => false} // block sensitive pastes
|
|
10
|
+
/>
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
## Contents
|
|
16
|
+
|
|
17
|
+
- [How it works](#how-it-works)
|
|
18
|
+
- [Installation](#installation)
|
|
19
|
+
- [Framework compatibility](#framework-compatibility)
|
|
20
|
+
- [Quick start](#quick-start)
|
|
21
|
+
- [Form library integration](#form-library-integration)
|
|
22
|
+
- [FieldShieldInput props](#fieldshieldinput-props)
|
|
23
|
+
- [Ref methods](#ref-methods)
|
|
24
|
+
- [Clipboard callbacks](#clipboard-callbacks)
|
|
25
|
+
- [maxProcessLength](#maxprocesslength)
|
|
26
|
+
- [Custom patterns](#custom-patterns)
|
|
27
|
+
- [Accessibility mode](#accessibility-mode)
|
|
28
|
+
- [useSecurityLog](#usesecuritylog)
|
|
29
|
+
- [collectSecureValues](#collectsecurevalues)
|
|
30
|
+
- [Built-in patterns](#built-in-patterns)
|
|
31
|
+
- [Content Security Policy](#content-security-policy)
|
|
32
|
+
- [Security architecture](#security-architecture)
|
|
33
|
+
- [Known limitations](#known-limitations)
|
|
34
|
+
- [Versioning and pattern updates](#versioning-and-pattern-updates)
|
|
35
|
+
- [TypeScript](#typescript)
|
|
36
|
+
- [Compliance notes](#compliance-notes)
|
|
37
|
+
|
|
38
|
+
---
|
|
39
|
+
|
|
40
|
+
## How it works
|
|
41
|
+
|
|
42
|
+
FieldShield protects against three attack vectors:
|
|
43
|
+
|
|
44
|
+
**DOM scraping** — Browser extensions, session recording tools (FullStory, LogRocket), and automated scrapers read `input.value` from the DOM. FieldShield stores the real value in an isolated Web Worker thread and writes only scrambled `x` characters to `input.value`. The DOM never contains the real value.
|
|
45
|
+
|
|
46
|
+
**Clipboard exfiltration** — Users accidentally copy sensitive text into LLMs, email clients, or unsecured applications. FieldShield intercepts copy and cut events and writes masked content (`█` characters) to the clipboard instead of the real value. The selection indices are preserved so partial copies also produce masked output.
|
|
47
|
+
|
|
48
|
+
**Paste exposure** — Sensitive data pasted from another source lands in the DOM and may be captured by recording tools. FieldShield intercepts paste events, scans the pasted content against all active patterns, and fires `onSensitivePaste` with the findings. Returning `false` from the callback blocks the paste entirely.
|
|
49
|
+
|
|
50
|
+
---
|
|
51
|
+
|
|
52
|
+
## Installation
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
npm install fieldshield
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
FieldShield requires React 18 or later.
|
|
59
|
+
|
|
60
|
+
Import the stylesheet once in your application entry point — FieldShield's component styles will not apply without it:
|
|
61
|
+
|
|
62
|
+
```ts
|
|
63
|
+
import "fieldshield/dist/assets/fieldshield.css";
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
---
|
|
67
|
+
|
|
68
|
+
## Framework compatibility
|
|
69
|
+
|
|
70
|
+
FieldShield uses the `new URL('./fieldshield.worker.ts', import.meta.url)` pattern to instantiate its Web Worker. This is supported natively in Vite and in Webpack 5+ with no additional configuration.
|
|
71
|
+
|
|
72
|
+
### Vite
|
|
73
|
+
|
|
74
|
+
Works out of the box. No configuration required.
|
|
75
|
+
|
|
76
|
+
### Webpack 5
|
|
77
|
+
|
|
78
|
+
Works out of the box with Webpack 5's built-in Web Worker support.
|
|
79
|
+
|
|
80
|
+
### Webpack 4
|
|
81
|
+
|
|
82
|
+
Requires `worker-loader`:
|
|
83
|
+
|
|
84
|
+
```bash
|
|
85
|
+
npm install --save-dev worker-loader
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
```js
|
|
89
|
+
// webpack.config.js
|
|
90
|
+
module.exports = {
|
|
91
|
+
module: {
|
|
92
|
+
rules: [
|
|
93
|
+
{
|
|
94
|
+
test: /\.worker\.ts$/,
|
|
95
|
+
use: { loader: "worker-loader" },
|
|
96
|
+
},
|
|
97
|
+
],
|
|
98
|
+
},
|
|
99
|
+
};
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
### Next.js
|
|
103
|
+
|
|
104
|
+
Next.js requires explicit worker configuration. Add the following to `next.config.js`:
|
|
105
|
+
|
|
106
|
+
```js
|
|
107
|
+
// next.config.js
|
|
108
|
+
module.exports = {
|
|
109
|
+
webpack(config) {
|
|
110
|
+
config.output.publicPath = "/_next/";
|
|
111
|
+
return config;
|
|
112
|
+
},
|
|
113
|
+
};
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
If you encounter issues with the worker URL resolution in Next.js, use the `NEXT_PUBLIC_` environment variable pattern to set the base URL explicitly, or open an issue — Next.js worker support is an active area of improvement.
|
|
117
|
+
|
|
118
|
+
### Server-Side Rendering (SSR)
|
|
119
|
+
|
|
120
|
+
Web Workers are browser-only APIs. FieldShieldInput will throw if rendered on the server. Wrap it in a dynamic import with `ssr: false` in Next.js:
|
|
121
|
+
|
|
122
|
+
```tsx
|
|
123
|
+
import dynamic from "next/dynamic";
|
|
124
|
+
|
|
125
|
+
const FieldShieldInput = dynamic(
|
|
126
|
+
() => import("fieldshield").then((m) => m.FieldShieldInput),
|
|
127
|
+
{ ssr: false },
|
|
128
|
+
);
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
### Browser extension conflicts
|
|
132
|
+
|
|
133
|
+
Some browser extensions inject content into form fields and may conflict with FieldShieldInput's scrambling overlay:
|
|
134
|
+
|
|
135
|
+
- **Grammarly** — injects spell-check nodes that attempt to correct scrambled `x` characters. FieldShieldInput sets `spellcheck="false"` automatically, but if you see Grammarly interference add `data-gramm="false" data-gramm_editor="false"` to the container via the `className` or wrap with a div containing those attributes.
|
|
136
|
+
- **LastPass / 1Password** — these tools look for `type="password"` fields. FieldShieldInput is not a password field and will not trigger autofill, which is correct behavior — users should not autofill SSNs or clinical notes.
|
|
137
|
+
|
|
138
|
+
### React 19
|
|
139
|
+
|
|
140
|
+
FieldShield works with React 19 without any configuration. The library uses
|
|
141
|
+
`forwardRef` internally which is deprecated but fully functional in React 19.
|
|
142
|
+
A migration to React 19's ref-as-prop pattern is planned for v1.1.
|
|
143
|
+
|
|
144
|
+
---
|
|
145
|
+
|
|
146
|
+
## Quick start
|
|
147
|
+
|
|
148
|
+
```tsx
|
|
149
|
+
import { useRef } from "react";
|
|
150
|
+
import { FieldShieldInput } from "fieldshield";
|
|
151
|
+
import type { FieldShieldHandle } from "fieldshield";
|
|
152
|
+
import "fieldshield/dist/assets/fieldshield.css";
|
|
153
|
+
|
|
154
|
+
export function PatientForm() {
|
|
155
|
+
const ssnRef = useRef<FieldShieldHandle>(null);
|
|
156
|
+
|
|
157
|
+
const handleSubmit = async () => {
|
|
158
|
+
// Real value retrieved from isolated worker memory — never from the DOM
|
|
159
|
+
const ssn = await ssnRef.current?.getSecureValue();
|
|
160
|
+
await fetch("/api/patient", { body: JSON.stringify({ ssn }) });
|
|
161
|
+
|
|
162
|
+
// Zero out worker memory after submission
|
|
163
|
+
ssnRef.current?.purge();
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
return (
|
|
167
|
+
<FieldShieldInput
|
|
168
|
+
ref={ssnRef}
|
|
169
|
+
label="Social Security Number"
|
|
170
|
+
inputMode="numeric"
|
|
171
|
+
maxLength={11}
|
|
172
|
+
onSensitiveCopyAttempt={(e) => console.warn("Copy blocked:", e.findings)}
|
|
173
|
+
/>
|
|
174
|
+
);
|
|
175
|
+
}
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
---
|
|
179
|
+
|
|
180
|
+
## Form library integration
|
|
181
|
+
|
|
182
|
+
### React Hook Form
|
|
183
|
+
|
|
184
|
+
React Hook Form's `register()` expects to read `e.target.value` synchronously on every keystroke. Because FieldShieldInput writes only scrambled `x` characters to `input.value`, standard `register()` will validate `"xxxxxxxxxxxx"` rather than the real value.
|
|
185
|
+
|
|
186
|
+
The correct pattern is to use RHF's `Controller` component and validate on submit using `getSecureValue()`:
|
|
187
|
+
|
|
188
|
+
```tsx
|
|
189
|
+
import { useRef } from "react";
|
|
190
|
+
import { useForm, Controller } from "react-hook-form";
|
|
191
|
+
import { FieldShieldInput, collectSecureValues } from "fieldshield";
|
|
192
|
+
import type { FieldShieldHandle } from "fieldshield";
|
|
193
|
+
|
|
194
|
+
export function PatientForm() {
|
|
195
|
+
const { handleSubmit, control, setError } = useForm();
|
|
196
|
+
const ssnRef = useRef<FieldShieldHandle>(null);
|
|
197
|
+
|
|
198
|
+
const onSubmit = async () => {
|
|
199
|
+
const { ssn } = await collectSecureValues({ ssn: ssnRef });
|
|
200
|
+
|
|
201
|
+
// Validate the real value here
|
|
202
|
+
if (!ssn.match(/^\d{3}-\d{2}-\d{4}$/)) {
|
|
203
|
+
setError("ssn", { message: "Invalid SSN format" });
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
await fetch("/api/patient", { body: JSON.stringify({ ssn }) });
|
|
208
|
+
ssnRef.current?.purge();
|
|
209
|
+
};
|
|
210
|
+
|
|
211
|
+
return (
|
|
212
|
+
<form onSubmit={handleSubmit(onSubmit)}>
|
|
213
|
+
<Controller
|
|
214
|
+
name="ssn"
|
|
215
|
+
control={control}
|
|
216
|
+
render={() => (
|
|
217
|
+
<FieldShieldInput
|
|
218
|
+
ref={ssnRef}
|
|
219
|
+
label="Social Security Number"
|
|
220
|
+
inputMode="numeric"
|
|
221
|
+
maxLength={11}
|
|
222
|
+
/>
|
|
223
|
+
)}
|
|
224
|
+
/>
|
|
225
|
+
<button type="submit">Submit</button>
|
|
226
|
+
</form>
|
|
227
|
+
);
|
|
228
|
+
}
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
The key shift is moving from synchronous per-keystroke validation to async on-submit validation. This is the correct mental model for any field where the value lives in isolated memory — validate at the point of use, not at the point of entry.
|
|
232
|
+
|
|
233
|
+
### Why not validate on every keystroke?
|
|
234
|
+
|
|
235
|
+
Standard form libraries validate on `onChange` using `e.target.value`. Because the real value lives in worker memory and `e.target.value` only contains scrambled characters, per-keystroke validation of the real value would require a `GET_TRUTH` round-trip on every keystroke — one async operation per character typed. This creates unnecessary load on the worker and introduces the async overhead that RHF's synchronous model is designed to avoid.
|
|
236
|
+
|
|
237
|
+
The recommended pattern is: validate format constraints via `maxLength` and `inputMode` during input, then validate the real value's content on submit.
|
|
238
|
+
|
|
239
|
+
### Formik
|
|
240
|
+
|
|
241
|
+
Same pattern as RHF — use `setFieldValue` in the submit handler after retrieving the real value:
|
|
242
|
+
|
|
243
|
+
```tsx
|
|
244
|
+
const formik = useFormik({
|
|
245
|
+
initialValues: { ssn: "" },
|
|
246
|
+
onSubmit: async () => {
|
|
247
|
+
const { ssn } = await collectSecureValues({ ssn: ssnRef });
|
|
248
|
+
if (!isValidSSN(ssn)) {
|
|
249
|
+
formik.setFieldError("ssn", "Invalid SSN format");
|
|
250
|
+
return;
|
|
251
|
+
}
|
|
252
|
+
await submitToBackend({ ssn });
|
|
253
|
+
ssnRef.current?.purge();
|
|
254
|
+
},
|
|
255
|
+
});
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
### Zod
|
|
259
|
+
|
|
260
|
+
Zod validation works naturally at the submit boundary:
|
|
261
|
+
|
|
262
|
+
```tsx
|
|
263
|
+
const schema = z.object({
|
|
264
|
+
ssn: z.string().regex(/^\d{3}-\d{2}-\d{4}$/, "Invalid SSN format"),
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
const onSubmit = async () => {
|
|
268
|
+
const values = await collectSecureValues({ ssn: ssnRef });
|
|
269
|
+
const result = schema.safeParse(values);
|
|
270
|
+
if (!result.success) {
|
|
271
|
+
// handle errors
|
|
272
|
+
return;
|
|
273
|
+
}
|
|
274
|
+
await submitToBackend(result.data);
|
|
275
|
+
};
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
---
|
|
279
|
+
|
|
280
|
+
### `label`
|
|
281
|
+
|
|
282
|
+
`string` — optional
|
|
283
|
+
|
|
284
|
+
Visible label text rendered above the field and linked via `htmlFor`/`id`. Also used as the field identifier in clipboard event payloads. When omitted no `<label>` is rendered and the field falls back to `"Protected field"` for screen reader announcements.
|
|
285
|
+
|
|
286
|
+
### `type`
|
|
287
|
+
|
|
288
|
+
`"text" | "textarea"` — default `"text"`
|
|
289
|
+
|
|
290
|
+
Renders a single-line `<input>` or a multi-line `<textarea>`. Textarea mode enables auto-grow — the field expands vertically as the user types past the initial height.
|
|
291
|
+
|
|
292
|
+
### `placeholder`
|
|
293
|
+
|
|
294
|
+
`string` — optional
|
|
295
|
+
|
|
296
|
+
Forwarded to the native `placeholder` attribute. Displayed in the mask layer when the field is empty.
|
|
297
|
+
|
|
298
|
+
### `disabled`
|
|
299
|
+
|
|
300
|
+
`boolean` — default `false`
|
|
301
|
+
|
|
302
|
+
Disables the field. Sets `data-disabled` on the container for CSS styling hooks.
|
|
303
|
+
|
|
304
|
+
### `required`
|
|
305
|
+
|
|
306
|
+
`boolean` — default `false`
|
|
307
|
+
|
|
308
|
+
Sets `aria-required` on the input so screen readers announce the field as mandatory.
|
|
309
|
+
|
|
310
|
+
### `maxLength`
|
|
311
|
+
|
|
312
|
+
`number` — optional
|
|
313
|
+
|
|
314
|
+
Native HTML `maxLength`. Use this for structured fields with known lengths — SSN (11), credit card (19), IBAN (34 max). Enforced by the browser before FieldShield processes the input.
|
|
315
|
+
|
|
316
|
+
### `rows`
|
|
317
|
+
|
|
318
|
+
`number` — default `3`
|
|
319
|
+
|
|
320
|
+
Initial visible row count. Only applies when `type="textarea"`. The field still auto-grows beyond this value.
|
|
321
|
+
|
|
322
|
+
### `inputMode`
|
|
323
|
+
|
|
324
|
+
`"text" | "numeric" | "decimal" | "tel" | "email" | "search" | "url" | "none"` — default `"text"`
|
|
325
|
+
|
|
326
|
+
Mobile keyboard hint. Does not affect value handling — the field always operates as `type="text"` internally to preserve DOM scrambling.
|
|
327
|
+
|
|
328
|
+
Use this instead of `type="number"` or `type="email"` — those change browser validation and value parsing in ways that break DOM scrambling.
|
|
329
|
+
|
|
330
|
+
```tsx
|
|
331
|
+
<FieldShieldInput inputMode="numeric" label="SSN" />
|
|
332
|
+
<FieldShieldInput inputMode="tel" label="Phone" />
|
|
333
|
+
```
|
|
334
|
+
|
|
335
|
+
### `className`
|
|
336
|
+
|
|
337
|
+
`string` — optional
|
|
338
|
+
|
|
339
|
+
Additional CSS class applied to the outermost container `<div>`, merged with the internal `fieldshield-container` class.
|
|
340
|
+
|
|
341
|
+
### `style`
|
|
342
|
+
|
|
343
|
+
`React.CSSProperties` — optional
|
|
344
|
+
|
|
345
|
+
Inline styles applied to the outermost container `<div>`.
|
|
346
|
+
|
|
347
|
+
### `onChange`
|
|
348
|
+
|
|
349
|
+
`(masked: string, findings: string[]) => void` — optional
|
|
350
|
+
|
|
351
|
+
Fires after each worker UPDATE response — whenever the masked value or findings change. Receives the masked display string and the current findings array. Never receives the real value.
|
|
352
|
+
|
|
353
|
+
```tsx
|
|
354
|
+
<FieldShieldInput
|
|
355
|
+
label="Notes"
|
|
356
|
+
onChange={(masked, findings) => {
|
|
357
|
+
if (findings.length > 0) setHasSensitiveData(true);
|
|
358
|
+
}}
|
|
359
|
+
/>
|
|
360
|
+
```
|
|
361
|
+
|
|
362
|
+
### `a11yMode`
|
|
363
|
+
|
|
364
|
+
`boolean` — default `false`
|
|
365
|
+
|
|
366
|
+
Disables DOM scrambling and renders a native `type="password"` input instead. Pattern detection and clipboard protection remain fully active.
|
|
367
|
+
|
|
368
|
+
Use this for WCAG 2.1 AA / Section 508 compliance — screen readers handle `type="password"` natively and cannot interact with the scrambled overlay used in standard mode. See [Accessibility mode](#accessibility-mode).
|
|
369
|
+
|
|
370
|
+
### `customPatterns`
|
|
371
|
+
|
|
372
|
+
`CustomPattern[]` — optional
|
|
373
|
+
|
|
374
|
+
Additional sensitive-data patterns layered on top of the built-in defaults. See [Custom patterns](#custom-patterns).
|
|
375
|
+
|
|
376
|
+
### `maxProcessLength`
|
|
377
|
+
|
|
378
|
+
`number` — default `100000`
|
|
379
|
+
|
|
380
|
+
Maximum number of characters sent to the worker for pattern detection. If the user types or pastes beyond this limit the input is **blocked** — the field reverts to its previous value.
|
|
381
|
+
|
|
382
|
+
Blocking rather than truncating is intentional. Truncation would create a blind spot where sensitive data beyond the limit is never scanned or protected.
|
|
383
|
+
|
|
384
|
+
> **Important:** Always wire up `onMaxLengthExceeded` for any field that uses `maxProcessLength`. Without it, the field silently stops accepting input with no explanation to the user.
|
|
385
|
+
|
|
386
|
+
```tsx
|
|
387
|
+
<FieldShieldInput
|
|
388
|
+
label="Clinical Notes"
|
|
389
|
+
type="textarea"
|
|
390
|
+
maxProcessLength={50_000}
|
|
391
|
+
onMaxLengthExceeded={(length, limit) =>
|
|
392
|
+
setError(`Maximum ${limit.toLocaleString()} characters reached`)
|
|
393
|
+
}
|
|
394
|
+
/>
|
|
395
|
+
```
|
|
396
|
+
|
|
397
|
+
This is distinct from `maxLength` — `maxLength` restricts the browser input, `maxProcessLength` caps worker processing. For structured fields with known lengths, use `maxLength`. For free-text fields where longer input is valid but should be bounded, use `maxProcessLength`.
|
|
398
|
+
|
|
399
|
+
### `onMaxLengthExceeded`
|
|
400
|
+
|
|
401
|
+
`(length: number, limit: number) => void` — optional
|
|
402
|
+
|
|
403
|
+
Called when input is blocked because it exceeds `maxProcessLength`. Use this to surface a character count warning or error message to the user.
|
|
404
|
+
|
|
405
|
+
A `console.warn` fires automatically even without this callback so developers see the block in DevTools.
|
|
406
|
+
|
|
407
|
+
### `onWorkerError`
|
|
408
|
+
|
|
409
|
+
`(error: ErrorEvent) => void` — optional
|
|
410
|
+
|
|
411
|
+
Called when the Web Worker encounters a runtime error. When this fires, FieldShieldInput has already reset `masked` and `findings` to empty so the field does not freeze showing stale warnings.
|
|
412
|
+
|
|
413
|
+
The worker is not terminated on error — a transient error may not affect subsequent messages. If errors persist, surface a warning and ask the user to refresh.
|
|
414
|
+
|
|
415
|
+
> **Note:** If the worker fails to initialize entirely (e.g. due to a strict CSP), the component automatically falls back to `a11yMode` — this callback is not called in that case. The fallback is silent by design but logged to `console.error`.
|
|
416
|
+
|
|
417
|
+
```tsx
|
|
418
|
+
<FieldShieldInput
|
|
419
|
+
label="SSN"
|
|
420
|
+
onWorkerError={(e) => {
|
|
421
|
+
console.error("Worker error:", e.message);
|
|
422
|
+
setFieldError("Worker unavailable — please refresh");
|
|
423
|
+
}}
|
|
424
|
+
/>
|
|
425
|
+
```
|
|
426
|
+
|
|
427
|
+
### `onFocus`
|
|
428
|
+
|
|
429
|
+
`(e: React.FocusEvent) => void` — optional
|
|
430
|
+
|
|
431
|
+
Forwarded from the underlying input element.
|
|
432
|
+
|
|
433
|
+
### `onBlur`
|
|
434
|
+
|
|
435
|
+
`(e: React.FocusEvent) => void` — optional
|
|
436
|
+
|
|
437
|
+
Forwarded from the underlying input element.
|
|
438
|
+
|
|
439
|
+
### `onSensitiveCopyAttempt`
|
|
440
|
+
|
|
441
|
+
`(event: SensitiveClipboardEvent) => void` — optional
|
|
442
|
+
|
|
443
|
+
Fired when the user copies or cuts from the field while sensitive patterns are present. The clipboard receives the masked text instead of the real value. Use this to surface a toast notification or write a security audit log.
|
|
444
|
+
|
|
445
|
+
### `onSensitivePaste`
|
|
446
|
+
|
|
447
|
+
`(event: SensitiveClipboardEvent) => boolean | void` — optional
|
|
448
|
+
|
|
449
|
+
Fired when the user pastes content that contains sensitive patterns.
|
|
450
|
+
|
|
451
|
+
Return `false` to block the paste — the field reverts to its previous value and the clipboard content is discarded. Return nothing or `true` to allow the paste to proceed.
|
|
452
|
+
|
|
453
|
+
```tsx
|
|
454
|
+
// Block sensitive pastes
|
|
455
|
+
onSensitivePaste={(e) => {
|
|
456
|
+
auditLog(e);
|
|
457
|
+
return false;
|
|
458
|
+
}}
|
|
459
|
+
|
|
460
|
+
// Allow sensitive pastes but log them
|
|
461
|
+
onSensitivePaste={(e) => {
|
|
462
|
+
auditLog(e);
|
|
463
|
+
// return nothing — paste proceeds
|
|
464
|
+
}}
|
|
465
|
+
```
|
|
466
|
+
|
|
467
|
+
---
|
|
468
|
+
|
|
469
|
+
## Ref methods
|
|
470
|
+
|
|
471
|
+
Attach a ref typed as `FieldShieldHandle` to access imperative methods.
|
|
472
|
+
|
|
473
|
+
```tsx
|
|
474
|
+
const ref = useRef<FieldShieldHandle>(null);
|
|
475
|
+
<FieldShieldInput ref={ref} label="SSN" />;
|
|
476
|
+
```
|
|
477
|
+
|
|
478
|
+
### `getSecureValue()`
|
|
479
|
+
|
|
480
|
+
`() => Promise<string>`
|
|
481
|
+
|
|
482
|
+
Retrieves the real, unmasked value from the worker's isolated memory via a private `MessageChannel`. The value travels point-to-point — browser extensions monitoring `postMessage` on the page cannot intercept it.
|
|
483
|
+
|
|
484
|
+
Rejects with a timeout error if the worker does not respond within 3 seconds.
|
|
485
|
+
|
|
486
|
+
**Always handle the rejection.** A rejected `getSecureValue()` means the worker is unavailable — the field value is lost and the form cannot be submitted safely. Do not silently swallow the error.
|
|
487
|
+
|
|
488
|
+
```ts
|
|
489
|
+
const handleSubmit = async () => {
|
|
490
|
+
try {
|
|
491
|
+
const value = await ref.current?.getSecureValue();
|
|
492
|
+
await fetch("/api/save", { body: JSON.stringify({ value }) });
|
|
493
|
+
} catch (err) {
|
|
494
|
+
// Worker timed out or was terminated — surface an error to the user
|
|
495
|
+
setSubmitError(
|
|
496
|
+
"Unable to retrieve field value securely. Please refresh and try again.",
|
|
497
|
+
);
|
|
498
|
+
return;
|
|
499
|
+
}
|
|
500
|
+
};
|
|
501
|
+
```
|
|
502
|
+
|
|
503
|
+
**Session timeout pattern** — for HIPAA compliance, call `purgeSecureValues` when the session expires to ensure worker memory is zeroed before the user is logged out:
|
|
504
|
+
|
|
505
|
+
```ts
|
|
506
|
+
// On session timeout or logout
|
|
507
|
+
const handleSessionEnd = () => {
|
|
508
|
+
purgeSecureValues(refs); // zero all workers simultaneously
|
|
509
|
+
redirectToLogin();
|
|
510
|
+
};
|
|
511
|
+
```
|
|
512
|
+
|
|
513
|
+
### `purge()`
|
|
514
|
+
|
|
515
|
+
`() => void`
|
|
516
|
+
|
|
517
|
+
Zeros out the stored value in worker memory. Call this immediately after `getSecureValue()` resolves and the data has been sent to your backend.
|
|
518
|
+
|
|
519
|
+
```ts
|
|
520
|
+
const value = await ref.current?.getSecureValue();
|
|
521
|
+
await sendToBackend(value);
|
|
522
|
+
ref.current?.purge(); // fire and forget
|
|
523
|
+
```
|
|
524
|
+
|
|
525
|
+
---
|
|
526
|
+
|
|
527
|
+
## Clipboard callbacks
|
|
528
|
+
|
|
529
|
+
Both `onSensitiveCopyAttempt` and `onSensitivePaste` receive a `SensitiveClipboardEvent` payload:
|
|
530
|
+
|
|
531
|
+
```ts
|
|
532
|
+
interface SensitiveClipboardEvent {
|
|
533
|
+
timestamp: string; // ISO 8601
|
|
534
|
+
fieldLabel: string; // the label prop value
|
|
535
|
+
findings: string[]; // e.g. ["SSN", "EMAIL"]
|
|
536
|
+
masked: string; // masked preview with █ characters
|
|
537
|
+
eventType: "copy" | "cut" | "paste";
|
|
538
|
+
}
|
|
539
|
+
```
|
|
540
|
+
|
|
541
|
+
`masked` contains only the selected/pasted portion with sensitive spans replaced by `█`. The length is preserved so the structure is visible — `"SSN: ███-██-████"` rather than a uniform block.
|
|
542
|
+
|
|
543
|
+
---
|
|
544
|
+
|
|
545
|
+
## maxProcessLength
|
|
546
|
+
|
|
547
|
+
The default of `100_000` characters is large enough for legitimate clinical notes and free-text fields while protecting against denial-of-service via adversarially crafted regex inputs.
|
|
548
|
+
|
|
549
|
+
For structured fields with known maximum lengths, use `maxLength` instead — the browser enforces it before FieldShield processes anything, which is more efficient.
|
|
550
|
+
|
|
551
|
+
```tsx
|
|
552
|
+
// Structured field — browser enforces 11 chars, worker never sees more
|
|
553
|
+
<FieldShieldInput label="SSN" maxLength={11} />
|
|
554
|
+
|
|
555
|
+
// Free text — worker processes up to 100k chars
|
|
556
|
+
<FieldShieldInput label="Clinical Notes" type="textarea" />
|
|
557
|
+
|
|
558
|
+
// Custom limit
|
|
559
|
+
<FieldShieldInput
|
|
560
|
+
label="Notes"
|
|
561
|
+
maxProcessLength={50_000}
|
|
562
|
+
onMaxLengthExceeded={(length, limit) =>
|
|
563
|
+
setError(`Input too long — maximum ${limit.toLocaleString()} characters`)
|
|
564
|
+
}
|
|
565
|
+
/>
|
|
566
|
+
```
|
|
567
|
+
|
|
568
|
+
---
|
|
569
|
+
|
|
570
|
+
## Custom patterns
|
|
571
|
+
|
|
572
|
+
Pass an array of `CustomPattern` objects to detect additional sensitive data types specific to your application.
|
|
573
|
+
|
|
574
|
+
```tsx
|
|
575
|
+
<FieldShieldInput
|
|
576
|
+
label="Employee Record"
|
|
577
|
+
customPatterns={[
|
|
578
|
+
{ name: "EMPLOYEE_ID", regex: "EMP-\\d{6}" },
|
|
579
|
+
{ name: "BADGE_NUMBER", regex: "\\bBDG-[A-Z]{2}\\d{4}\\b" },
|
|
580
|
+
]}
|
|
581
|
+
onSensitiveCopyAttempt={(e) => log(e.findings)}
|
|
582
|
+
/>
|
|
583
|
+
```
|
|
584
|
+
|
|
585
|
+
Custom patterns are layered on top of the built-in defaults — both sets run on every keystroke. If a custom pattern has the same name as a built-in, it overrides the built-in for that field.
|
|
586
|
+
|
|
587
|
+
```ts
|
|
588
|
+
interface CustomPattern {
|
|
589
|
+
name: string; // shown in findings arrays
|
|
590
|
+
regex: string; // regex source string — no delimiters, no flags
|
|
591
|
+
// use double backslashes: "\\d{6}" not "\d{6}"
|
|
592
|
+
}
|
|
593
|
+
```
|
|
594
|
+
|
|
595
|
+
The worker applies `gi` flags automatically. Order is preserved — patterns run in array order.
|
|
596
|
+
|
|
597
|
+
---
|
|
598
|
+
|
|
599
|
+
## Accessibility mode
|
|
600
|
+
|
|
601
|
+
Standard mode uses a DOM scrambling overlay that is invisible to sighted users but incompatible with some screen readers. Enable `a11yMode` for WCAG 2.1 AA / Section 508 compliance:
|
|
602
|
+
|
|
603
|
+
```tsx
|
|
604
|
+
<FieldShieldInput ref={ref} label="SSN" a11yMode />
|
|
605
|
+
```
|
|
606
|
+
|
|
607
|
+
In `a11yMode`:
|
|
608
|
+
|
|
609
|
+
- A native `type="password"` input is rendered
|
|
610
|
+
- The browser's built-in password masking handles visual output
|
|
611
|
+
- Pattern detection still runs through the worker on every keystroke
|
|
612
|
+
- Clipboard protection remains fully active
|
|
613
|
+
- The scrambling overlay is not rendered
|
|
614
|
+
|
|
615
|
+
Use `a11yMode` when your users rely on screen readers (VoiceOver, NVDA, JAWS) or when WCAG compliance is required.
|
|
616
|
+
|
|
617
|
+
---
|
|
618
|
+
|
|
619
|
+
## useSecurityLog
|
|
620
|
+
|
|
621
|
+
Maintains a capped, auto-timestamped log of FieldShield security events suitable for real-time audit displays and HIPAA audit trail requirements.
|
|
622
|
+
|
|
623
|
+
```tsx
|
|
624
|
+
import { useSecurityLog } from "fieldshield";
|
|
625
|
+
|
|
626
|
+
const { events, makeClipboardHandler, pushEvent, clearLog } = useSecurityLog({
|
|
627
|
+
maxEvents: 20, // default
|
|
628
|
+
});
|
|
629
|
+
|
|
630
|
+
<FieldShieldInput
|
|
631
|
+
label="SSN"
|
|
632
|
+
onSensitiveCopyAttempt={makeClipboardHandler("copy_cut")}
|
|
633
|
+
onSensitivePaste={makeClipboardHandler("paste")}
|
|
634
|
+
/>;
|
|
635
|
+
|
|
636
|
+
// Display the log
|
|
637
|
+
{
|
|
638
|
+
events.map((ev) => (
|
|
639
|
+
<div key={ev.id}>
|
|
640
|
+
{ev.timestamp} — {ev.type} — {ev.field} — {ev.findings.join(", ")}
|
|
641
|
+
</div>
|
|
642
|
+
));
|
|
643
|
+
}
|
|
644
|
+
```
|
|
645
|
+
|
|
646
|
+
### `makeClipboardHandler(context)`
|
|
647
|
+
|
|
648
|
+
Returns a ready-to-wire `SensitiveClipboardEvent` handler.
|
|
649
|
+
|
|
650
|
+
- Pass `"copy_cut"` for `onSensitiveCopyAttempt` — inspects `e.eventType` internally to distinguish `COPY_BLOCKED` from `CUT_BLOCKED`
|
|
651
|
+
- Pass `"paste"` for `onSensitivePaste` — maps to `PASTE_DETECTED`
|
|
652
|
+
|
|
653
|
+
### `pushEvent(event)`
|
|
654
|
+
|
|
655
|
+
Push any event manually — use for `SUBMIT` and `PURGE` events:
|
|
656
|
+
|
|
657
|
+
```ts
|
|
658
|
+
pushEvent({
|
|
659
|
+
field: "All fields",
|
|
660
|
+
type: "SUBMIT",
|
|
661
|
+
findings: [],
|
|
662
|
+
detail: "3 fields submitted",
|
|
663
|
+
});
|
|
664
|
+
```
|
|
665
|
+
|
|
666
|
+
### `clearLog()`
|
|
667
|
+
|
|
668
|
+
Empties the events array and resets the ID counter.
|
|
669
|
+
|
|
670
|
+
### Event types
|
|
671
|
+
|
|
672
|
+
`COPY_BLOCKED` | `CUT_BLOCKED` | `PASTE_DETECTED` | `SUBMIT` | `PURGE` | `CUSTOM`
|
|
673
|
+
|
|
674
|
+
### Event shape
|
|
675
|
+
|
|
676
|
+
```ts
|
|
677
|
+
interface SecurityEvent {
|
|
678
|
+
id: number; // auto-incrementing, stable React key
|
|
679
|
+
timestamp: string; // from Date.toLocaleTimeString()
|
|
680
|
+
field: string; // field label or custom identifier
|
|
681
|
+
type: SecurityEventType;
|
|
682
|
+
findings: string[]; // pattern names active at time of event
|
|
683
|
+
detail?: string; // truncated masked preview (32 chars)
|
|
684
|
+
}
|
|
685
|
+
```
|
|
686
|
+
|
|
687
|
+
---
|
|
688
|
+
|
|
689
|
+
## collectSecureValues
|
|
690
|
+
|
|
691
|
+
Retrieves real values from multiple FieldShieldInput fields in parallel via `Promise.allSettled`. No plaintext exists on the main thread until this call resolves.
|
|
692
|
+
|
|
693
|
+
```tsx
|
|
694
|
+
import { useRef } from "react";
|
|
695
|
+
import {
|
|
696
|
+
FieldShieldInput,
|
|
697
|
+
collectSecureValues,
|
|
698
|
+
purgeSecureValues,
|
|
699
|
+
} from "fieldshield";
|
|
700
|
+
import type { FieldShieldHandle } from "fieldshield";
|
|
701
|
+
|
|
702
|
+
export function PatientForm() {
|
|
703
|
+
const ssnRef = useRef<FieldShieldHandle>(null);
|
|
704
|
+
const notesRef = useRef<FieldShieldHandle>(null);
|
|
705
|
+
const emailRef = useRef<FieldShieldHandle>(null);
|
|
706
|
+
|
|
707
|
+
const refs = { ssn: ssnRef, notes: notesRef, email: emailRef };
|
|
708
|
+
|
|
709
|
+
const handleSubmit = async () => {
|
|
710
|
+
const values = await collectSecureValues(refs);
|
|
711
|
+
// values = { ssn: "123-45-6789", notes: "...", email: "..." }
|
|
712
|
+
|
|
713
|
+
await fetch("/api/patient", {
|
|
714
|
+
method: "POST",
|
|
715
|
+
body: JSON.stringify(values),
|
|
716
|
+
});
|
|
717
|
+
|
|
718
|
+
purgeSecureValues(refs); // zero all workers simultaneously
|
|
719
|
+
};
|
|
720
|
+
|
|
721
|
+
return (
|
|
722
|
+
<>
|
|
723
|
+
<FieldShieldInput ref={ssnRef} label="SSN" />
|
|
724
|
+
<FieldShieldInput ref={notesRef} label="Clinical Notes" type="textarea" />
|
|
725
|
+
<FieldShieldInput ref={emailRef} label="Email" />
|
|
726
|
+
<button onClick={handleSubmit}>Submit</button>
|
|
727
|
+
</>
|
|
728
|
+
);
|
|
729
|
+
}
|
|
730
|
+
```
|
|
731
|
+
|
|
732
|
+
Null or unmounted refs resolve to `""` rather than throwing — a missing optional field never blocks form submission. Rejected fields also resolve to `""` with a `console.warn` identifying the field name.
|
|
733
|
+
|
|
734
|
+
`purgeSecureValues` calls `purge()` on every ref simultaneously. It is fire-and-forget — no await needed. The PURGE message is processed after the GET_TRUTH reply because both travel through the same worker message queue in order.
|
|
735
|
+
|
|
736
|
+
---
|
|
737
|
+
|
|
738
|
+
## Built-in patterns
|
|
739
|
+
|
|
740
|
+
All patterns apply `gi` flags — case-insensitive and global. Patterns are designed for a security context: false negative rate is minimized over false positive rate because a missed sensitive value is worse than a false positive that briefly highlights a non-sensitive number.
|
|
741
|
+
|
|
742
|
+
**13 built-in patterns active by default.** Five additional patterns (`IBAN`, `DEA_NUMBER`, `SWIFT_BIC`, `NPI_NUMBER`, `PASSPORT_NUMBER`) are available as [opt-in patterns](#opt-in-patterns) due to high false positive rates in free-text fields.
|
|
743
|
+
|
|
744
|
+
### PII patterns
|
|
745
|
+
|
|
746
|
+
| Pattern | Matches |
|
|
747
|
+
| --------------- | ------------------------------------------------------------------------------------ |
|
|
748
|
+
| `SSN` | `123-45-6789` · `123 45 6789` · `123.45.6789` · `123456789` |
|
|
749
|
+
| `EMAIL` | RFC 5321 compatible — `user@example.com`, plus addressing, subdomains |
|
|
750
|
+
| `PHONE` | US all formats · `+44` UK · `+91` India · `+353` Ireland · `+86` China and more |
|
|
751
|
+
| `CREDIT_CARD` | Visa 16-digit · Mastercard · Amex 15-digit — with or without spaces/hyphens |
|
|
752
|
+
| `DATE_OF_BIRTH` | `MM/DD/YYYY` · `MM-DD-YYYY` · `MM.DD.YYYY` · `YYYY-MM-DD` · `YYYY/MM/DD` (1900–2099) |
|
|
753
|
+
| `TAX_ID` | EIN `12-3456789` · 9-digit no separator |
|
|
754
|
+
|
|
755
|
+
### Healthcare and international identifiers
|
|
756
|
+
|
|
757
|
+
| Pattern | Matches |
|
|
758
|
+
| -------- | --------------------------------------------------------------------------------- |
|
|
759
|
+
| `UK_NIN` | UK National Insurance Number — `AB 12 34 56 C` (spaced) or `AB123456C` (compact) |
|
|
760
|
+
|
|
761
|
+
### Credential patterns
|
|
762
|
+
|
|
763
|
+
These patterns are designed for developer-facing inputs — config panels, support chat, API key management UIs. Consumer-facing deployments can safely ignore them — a user entering their SSN will never trigger `GITHUB_TOKEN` or `JWT`.
|
|
764
|
+
|
|
765
|
+
| Pattern | Matches |
|
|
766
|
+
| ------------------- | --------------------------------------------------------------------------- |
|
|
767
|
+
| `AI_API_KEY` | OpenAI `sk-` (all generations) · Anthropic `sk-ant-api03-` · Google `AIza` |
|
|
768
|
+
| `AWS_ACCESS_KEY` | `AKIA` permanent · `ASIA` temporary credential prefix |
|
|
769
|
+
| `GITHUB_TOKEN` | `ghp_` · `gho_` · `ghs_` · `ghu_` · `github_pat_` |
|
|
770
|
+
| `STRIPE_KEY` | `sk_live_` · `sk_test_` · `pk_live_` · `pk_test_` · `rk_live_` · `rk_test_` |
|
|
771
|
+
| `JWT` | Three base64url segments starting with `eyJ` |
|
|
772
|
+
| `PRIVATE_KEY_BLOCK` | `-----BEGIN [RSA\|EC\|OPENSSH] PRIVATE KEY-----` |
|
|
773
|
+
|
|
774
|
+
### Overriding a built-in pattern
|
|
775
|
+
|
|
776
|
+
Pass a custom pattern with the same name to override the built-in for that field:
|
|
777
|
+
|
|
778
|
+
```tsx
|
|
779
|
+
// Replace the built-in SSN pattern with a stricter version for this field
|
|
780
|
+
<FieldShieldInput
|
|
781
|
+
customPatterns={[{ name: "SSN", regex: "\\b\\d{3}-\\d{2}-\\d{4}\\b" }]}
|
|
782
|
+
/>
|
|
783
|
+
```
|
|
784
|
+
|
|
785
|
+
---
|
|
786
|
+
|
|
787
|
+
## Opt-in patterns
|
|
788
|
+
|
|
789
|
+
Some patterns are too broad to enable on every field. These four are excluded from the defaults because their regex structure matches common non-sensitive strings in clinical notes, pharmacy systems, and general free-text:
|
|
790
|
+
|
|
791
|
+
| Pattern | Why it's opt-in |
|
|
792
|
+
| ----------------- | ----------------------------------------------------------------------------------------- |
|
|
793
|
+
| `IBAN` | Two letters + two digits + alphanumeric groups — matches lab accession numbers, lot codes |
|
|
794
|
+
| `DEA_NUMBER` | Two letters + seven digits — matches any pharmaceutical lot number (`AB1234567`) |
|
|
795
|
+
| `SWIFT_BIC` | Eight uppercase letters — matches common words ("NEPHROPATHY", "PENICILLIN") |
|
|
796
|
+
| `NPI_NUMBER` | Ten digits starting with 1 or 2 — matches timestamps, order IDs, phone numbers |
|
|
797
|
+
| `PASSPORT_NUMBER` | One or two letters + six to nine digits — matches ICD-10 codes, specimen IDs |
|
|
798
|
+
|
|
799
|
+
> **Only add these to fields where that specific data type is the expected input.** Adding `NPI_NUMBER` to a clinical notes field will flag nearly every number entered.
|
|
800
|
+
|
|
801
|
+
### Usage
|
|
802
|
+
|
|
803
|
+
`OPT_IN_PATTERNS` values are regex source strings (the same type as `customPatterns.regex`), not `RegExp` objects. Pass them directly:
|
|
804
|
+
|
|
805
|
+
```tsx
|
|
806
|
+
import { FieldShieldInput, OPT_IN_PATTERNS } from "fieldshield";
|
|
807
|
+
|
|
808
|
+
// Wire transfer form — a BIC code is the only expected value here
|
|
809
|
+
<FieldShieldInput
|
|
810
|
+
label="Bank (SWIFT/BIC)"
|
|
811
|
+
customPatterns={[{ name: "SWIFT_BIC", regex: OPT_IN_PATTERNS.SWIFT_BIC }]}
|
|
812
|
+
/>
|
|
813
|
+
|
|
814
|
+
// Provider credentialing form — an NPI is the only expected value here
|
|
815
|
+
<FieldShieldInput
|
|
816
|
+
label="Provider NPI"
|
|
817
|
+
customPatterns={[{ name: "NPI_NUMBER", regex: OPT_IN_PATTERNS.NPI_NUMBER }]}
|
|
818
|
+
/>
|
|
819
|
+
```
|
|
820
|
+
|
|
821
|
+
The 14 built-in patterns stay active — `customPatterns` layers on top of them, it does not replace them.
|
|
822
|
+
|
|
823
|
+
---
|
|
824
|
+
|
|
825
|
+
## Content Security Policy
|
|
826
|
+
|
|
827
|
+
FieldShield's worker isolation guarantee can be enforced at the infrastructure level using Content Security Policy headers. Add the following directives to your CSP:
|
|
828
|
+
|
|
829
|
+
```
|
|
830
|
+
Content-Security-Policy:
|
|
831
|
+
worker-src 'self';
|
|
832
|
+
script-src 'self';
|
|
833
|
+
```
|
|
834
|
+
|
|
835
|
+
**`worker-src 'self' blob:`** — restricts Web Workers to same-origin scripts and blob URLs. The `blob:` source is required if you use the pre-compiled worker option (v1.1 roadmap). If you are certain you will only ever use the default source-file worker, `worker-src 'self'` without `blob:` is stricter.
|
|
836
|
+
|
|
837
|
+
**`script-src 'self'`** — restricts all script execution to same-origin. Combined with `worker-src`, this ensures neither the main thread nor the worker can load or execute scripts from external origins.
|
|
838
|
+
|
|
839
|
+
### No-network guarantee
|
|
840
|
+
|
|
841
|
+
The FieldShield worker makes no network requests of any kind. It contains no calls to `fetch()`, `XMLHttpRequest`, `WebSocket`, `EventSource`, or `navigator.sendBeacon()`. Communication is exclusively via `postMessage` with the main thread.
|
|
842
|
+
|
|
843
|
+
This guarantee is verifiable by inspecting `fieldshield.worker.ts` directly — the file has zero imports and zero network API calls. The `@security NO NETWORK ACCESS` comment at the top of the worker file is intended for auditors who need documented evidence of this property.
|
|
844
|
+
|
|
845
|
+
### Full recommended CSP for FieldShield deployments
|
|
846
|
+
|
|
847
|
+
```
|
|
848
|
+
Content-Security-Policy:
|
|
849
|
+
default-src 'self';
|
|
850
|
+
script-src 'self';
|
|
851
|
+
worker-src 'self' blob:;
|
|
852
|
+
connect-src 'self' https://your-api.example.com;
|
|
853
|
+
style-src 'self' 'unsafe-inline';
|
|
854
|
+
img-src 'self' data:;
|
|
855
|
+
frame-ancestors 'none';
|
|
856
|
+
```
|
|
857
|
+
|
|
858
|
+
Adjust `connect-src` to include only the API endpoints your application needs. The `frame-ancestors 'none'` directive prevents clickjacking attacks on forms containing sensitive fields.
|
|
859
|
+
|
|
860
|
+
---
|
|
861
|
+
|
|
862
|
+
## Security architecture
|
|
863
|
+
|
|
864
|
+
### Web Worker isolation
|
|
865
|
+
|
|
866
|
+
The real input value (`internalTruth`) lives exclusively in a dedicated Web Worker thread. It is never serialized to the main thread except through a private `MessageChannel` in response to an explicit `GET_TRUTH` message. Browser extensions monitoring `postMessage` on the page cannot intercept `MessageChannel` port messages because they are point-to-point, not broadcast.
|
|
867
|
+
|
|
868
|
+
### DOM scrambling
|
|
869
|
+
|
|
870
|
+
Every non-newline character in `input.value` is replaced with `x`. The DOM always contains scrambled content. Screen scrapers, browser extensions reading `.value`, and session recording tools see only `xxxxxxxxxx`. The `x` characters are replaced by an absolutely-positioned transparent layer — sighted users see the masked output from the worker, keyboard events and cursor positioning happen on the transparent real input.
|
|
871
|
+
|
|
872
|
+
### Clipboard masking
|
|
873
|
+
|
|
874
|
+
Copy and cut events are intercepted via `onCopy`/`onCut` handlers. When sensitive patterns are present, `e.clipboardData.setData()` writes the masked string (with `█` characters) rather than the real value. The selection range is mapped from the scrambled DOM coordinates to the real value coordinates to produce accurate partial-selection masking.
|
|
875
|
+
|
|
876
|
+
### Paste scanning
|
|
877
|
+
|
|
878
|
+
Paste events are intercepted before the browser inserts clipboard content. The pasted text is scanned synchronously against all active patterns using the same pattern source strings the worker uses — guaranteeing the pre-scan is always in sync with the worker scan. The `onSensitivePaste` callback fires before the paste lands, allowing real-time audit logging.
|
|
879
|
+
|
|
880
|
+
### Memory purge
|
|
881
|
+
|
|
882
|
+
The `PURGE` message zeros `internalTruth` in worker memory and posts a `PURGED` confirmation. This provides demonstrable evidence of data disposal for HIPAA and PCI-DSS compliance audits.
|
|
883
|
+
|
|
884
|
+
---
|
|
885
|
+
|
|
886
|
+
## Known limitations
|
|
887
|
+
|
|
888
|
+
### `realValueRef` on the main thread
|
|
889
|
+
|
|
890
|
+
While the user is actively typing, the real value exists in both worker memory (`internalTruth`) and a React ref on the main thread (`realValueRef`). This ref is required to reconstruct the real value from DOM events — without it, character-by-character editing would be impossible.
|
|
891
|
+
|
|
892
|
+
In React DevTools, a sufficiently privileged browser extension, or a debugger attached to the page, `realValueRef.current` is readable. The worker isolation guarantee applies fully at rest — when the user is not typing — and provides strong protection against passive DOM scraping. It does not protect against an active attacker with debugger access to the page.
|
|
893
|
+
|
|
894
|
+
### `getSecureValue()` on unmount
|
|
895
|
+
|
|
896
|
+
If `getSecureValue()` is called after the component unmounts, the worker has already been terminated and `workerRef.current` is null. The call resolves to `""` immediately. Always call `getSecureValue()` before triggering any navigation or unmount.
|
|
897
|
+
|
|
898
|
+
### `onBlur` / `handleBlur` zeroing `realValueRef`
|
|
899
|
+
|
|
900
|
+
FieldShield does not zero `realValueRef` on blur. The async `getSecureValue()` call on re-focus creates a race condition — the user might focus the field and immediately submit before the worker responds. The value is preserved across focus changes. Document this expectation in your threat model if required by your compliance framework.
|
|
901
|
+
|
|
902
|
+
### IME composition (CJK input)
|
|
903
|
+
|
|
904
|
+
Composed input via Input Method Editors (Chinese, Japanese, Korean) is not supported in v1. Characters entered via IME may not be reconstructed correctly in `realValueRef`. Planned for v1.1.
|
|
905
|
+
|
|
906
|
+
### Voice dictation
|
|
907
|
+
|
|
908
|
+
Third-party voice dictation software (Dragon NaturallySpeaking, etc.) injects text via OS-level events rather than standard DOM events. FieldShield cannot guarantee correct value reconstruction for voice-dictated input.
|
|
909
|
+
|
|
910
|
+
### Drag-and-drop text
|
|
911
|
+
|
|
912
|
+
Dragging text within the field to rearrange it is not supported. The reconstructed real value may be incorrect after an in-field drag operation.
|
|
913
|
+
|
|
914
|
+
### Tab character
|
|
915
|
+
|
|
916
|
+
Tab characters in textarea fields produce a visual drift between the mask layer and the real input cursor position. The stored real value remains correct — only the visual alignment is affected.
|
|
917
|
+
|
|
918
|
+
### No `name` prop — native form submission not supported
|
|
919
|
+
|
|
920
|
+
FieldShieldInput does not accept a `name` prop and does not support native HTML form submission via `<form>`. Because `input.value` always contains scrambled `x` characters, a native form submit would send garbage to the server.
|
|
921
|
+
|
|
922
|
+
Always use `getSecureValue()` or `collectSecureValues()` on submit — never rely on the DOM value.
|
|
923
|
+
|
|
924
|
+
### No `id` prop override
|
|
925
|
+
|
|
926
|
+
FieldShieldInput generates its own stable `id` via React's `useId()` hook to prevent collisions when multiple instances share a page. You cannot set a custom `id` on the underlying input element. If you need to target the input externally (e.g. for testing selectors), use `aria-label` or the container's `className` prop instead.
|
|
927
|
+
|
|
928
|
+
### `onCopy` and `onCut` props not forwarded
|
|
929
|
+
|
|
930
|
+
FieldShieldInput intercepts `copy` and `cut` events internally to implement clipboard masking. Consumer-provided `onCopy` and `onCut` props are not forwarded — they would silently do nothing. Use `onSensitiveCopyAttempt` instead, which fires after the masking has been applied and the clipboard has been written.
|
|
931
|
+
|
|
932
|
+
### Cross-field sensitive data combination
|
|
933
|
+
|
|
934
|
+
FieldShield detects sensitive patterns within a single field. It does not detect combinations across fields — for example, a first name in one field and an SSN in another that together constitute a HIPAA minimum necessary data set. Cross-field combination detection requires application-level logic. Use `onChange` to receive findings from each field and implement your own combination rules.
|
|
935
|
+
|
|
936
|
+
```tsx
|
|
937
|
+
// Example: detect SSN in any field on the form
|
|
938
|
+
const [formHasSensitiveData, setFormHasSensitiveData] = useState(false);
|
|
939
|
+
|
|
940
|
+
<FieldShieldInput
|
|
941
|
+
label="Notes"
|
|
942
|
+
onChange={(_, findings) => {
|
|
943
|
+
if (findings.includes("SSN")) setFormHasSensitiveData(true);
|
|
944
|
+
}}
|
|
945
|
+
/>;
|
|
946
|
+
```
|
|
947
|
+
|
|
948
|
+
### Names, addresses, and unstructured PHI
|
|
949
|
+
|
|
950
|
+
FieldShield detects structured sensitive data — values with a recognisable format like SSNs, credit card numbers, and API keys. It does not detect unstructured PHI such as:
|
|
951
|
+
|
|
952
|
+
- Patient names (`"John Smith"`)
|
|
953
|
+
- Street addresses (`"123 Main Street, Boston MA"`)
|
|
954
|
+
- Facility names, physician names, employer names
|
|
955
|
+
- Free-text clinical descriptions
|
|
956
|
+
|
|
957
|
+
These cannot be detected reliably with regex — they require NLP-based Named Entity Recognition (NER). Regex cannot distinguish `"John Smith"` (a patient name) from a company name or product name without semantic context.
|
|
958
|
+
|
|
959
|
+
**What this means for HIPAA deployments:** HIPAA's Safe Harbor de-identification method lists 18 identifier categories that must be removed or replaced. FieldShield covers several — SSN, EMAIL, PHONE, DATE_OF_BIRTH, NPI, DEA — but does not cover names, geographic data below state level, or device identifiers. FieldShield is a defence-in-depth control for structured identifiers — it does not constitute complete HIPAA de-identification on its own.
|
|
960
|
+
|
|
961
|
+
Applications handling free-text clinical notes should implement server-side NER-based PHI detection in addition to FieldShield's client-side structured pattern detection.
|
|
962
|
+
|
|
963
|
+
### PHI context-dependency
|
|
964
|
+
|
|
965
|
+
Some identifiers that appear individually non-sensitive become PHI when combined with other data in the same field. Two examples from FieldShield's pattern set:
|
|
966
|
+
|
|
967
|
+
**NPI numbers** are publicly searchable via the CMS NPPES registry — a provider's NPI alone is not sensitive. But `"Patient referred to NPI 1234567893 for oncology follow-up"` is a PHI-containing clinical note. FieldShield detects the NPI as a signal that the field likely contains a PHI combination.
|
|
968
|
+
|
|
969
|
+
**SWIFT/BIC codes** identify banks, not individuals. But a field containing `"Wire to DEUTDEDBBER account DE89370400440532013000"` is a sensitive financial record. FieldShield detects the SWIFT code as a signal that the field likely contains a wire transfer instruction.
|
|
970
|
+
|
|
971
|
+
The library's philosophy is that false negatives are worse than false positives in a security context — detecting a non-sensitive identifier that appears in a sensitive context is preferable to missing a sensitive combination entirely.
|
|
972
|
+
|
|
973
|
+
## Versioning and pattern updates
|
|
974
|
+
|
|
975
|
+
FieldShield follows semantic versioning:
|
|
976
|
+
|
|
977
|
+
- **Patch** (`1.0.x`) — bug fixes, false positive/negative corrections to existing patterns
|
|
978
|
+
- **Minor** (`1.x.0`) — new patterns, new props, new features — backwards compatible
|
|
979
|
+
- **Major** (`x.0.0`) — breaking API changes
|
|
980
|
+
|
|
981
|
+
**Pattern updates are minor releases, not patches.** A new pattern could start flagging content in a field that was previously clean, which affects application behavior. Treat pattern updates as you would any minor dependency upgrade — review the CHANGELOG before updating.
|
|
982
|
+
|
|
983
|
+
**Pinning patterns** — if your application requires a frozen pattern set (e.g. for a compliance audit that was performed against a specific version), pin your FieldShield version explicitly:
|
|
984
|
+
|
|
985
|
+
```json
|
|
986
|
+
"dependencies": {
|
|
987
|
+
"fieldshield": "1.0.4"
|
|
988
|
+
}
|
|
989
|
+
```
|
|
990
|
+
|
|
991
|
+
See [CHANGELOG.md](./CHANGELOG.md) for a full history of pattern changes and API updates.
|
|
992
|
+
|
|
993
|
+
---
|
|
994
|
+
|
|
995
|
+
## TypeScript
|
|
996
|
+
|
|
997
|
+
All types are exported from the package root:
|
|
998
|
+
|
|
999
|
+
```ts
|
|
1000
|
+
import type {
|
|
1001
|
+
FieldShieldHandle,
|
|
1002
|
+
FieldShieldInputProps,
|
|
1003
|
+
SensitiveClipboardEvent,
|
|
1004
|
+
CustomPattern,
|
|
1005
|
+
} from "fieldshield";
|
|
1006
|
+
|
|
1007
|
+
import type {
|
|
1008
|
+
SecurityEvent,
|
|
1009
|
+
SecurityEventType,
|
|
1010
|
+
UseSecurityLogOptions,
|
|
1011
|
+
UseSecurityLogReturn,
|
|
1012
|
+
} from "fieldshield";
|
|
1013
|
+
|
|
1014
|
+
import type { FieldShieldRefMap, SecureValues } from "fieldshield";
|
|
1015
|
+
```
|
|
1016
|
+
|
|
1017
|
+
FieldShield is written in strict TypeScript. All public APIs are fully typed with no `any`.
|
|
1018
|
+
|
|
1019
|
+
---
|
|
1020
|
+
|
|
1021
|
+
## Compliance notes
|
|
1022
|
+
|
|
1023
|
+
### HIPAA
|
|
1024
|
+
|
|
1025
|
+
FieldShield provides technical safeguards relevant to the HIPAA Security Rule (45 CFR § 164.312):
|
|
1026
|
+
|
|
1027
|
+
- **Access controls** — real values are only retrievable via `getSecureValue()`, not readable from the DOM
|
|
1028
|
+
- **Audit controls** — `useSecurityLog` provides structured, timestamped records of clipboard operations, form submissions, and memory purges
|
|
1029
|
+
- **Transmission security** — values never travel over `postMessage` broadcast channels; `MessageChannel` is point-to-point
|
|
1030
|
+
|
|
1031
|
+
FieldShield is a technical control, not a compliance attestation. It must be used as part of a broader HIPAA compliance program that includes administrative and physical safeguards.
|
|
1032
|
+
|
|
1033
|
+
### PCI-DSS
|
|
1034
|
+
|
|
1035
|
+
FieldShield addresses PCI-DSS Requirement 6.4 (protect web-facing applications) by preventing cardholder data from appearing in the DOM where it could be captured by browser-based skimmers. The `CREDIT_CARD` pattern covers Visa, Mastercard, and Amex in all common formats.
|
|
1036
|
+
|
|
1037
|
+
### SOC 2
|
|
1038
|
+
|
|
1039
|
+
The `PURGE` mechanism provides demonstrable evidence of data disposal. The `useSecurityLog` hook provides an audit trail that can be shipped to a backend logging service for SOC 2 Type II evidence collection.
|
|
1040
|
+
|
|
1041
|
+
---
|
|
1042
|
+
|
|
1043
|
+
## License
|
|
1044
|
+
|
|
1045
|
+
MIT
|