@transferwise/components 0.0.0-experimental-c388243 → 0.0.0-experimental-18063d6

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.
Files changed (75) hide show
  1. package/build/chips/Chip.js.map +1 -1
  2. package/build/chips/Chip.mjs.map +1 -1
  3. package/build/chips/Chips.js.map +1 -1
  4. package/build/chips/Chips.mjs.map +1 -1
  5. package/build/index.js +0 -2
  6. package/build/index.js.map +1 -1
  7. package/build/index.mjs +0 -1
  8. package/build/index.mjs.map +1 -1
  9. package/build/main.css +0 -35
  10. package/build/styles/main.css +0 -35
  11. package/build/types/body/Body.d.ts +2 -2
  12. package/build/types/chips/Chip.d.ts +3 -3
  13. package/build/types/chips/Chip.d.ts.map +1 -1
  14. package/build/types/chips/Chips.d.ts +10 -9
  15. package/build/types/chips/Chips.d.ts.map +1 -1
  16. package/build/types/iconButton/IconButton.d.ts +1 -1
  17. package/build/types/index.d.ts +0 -2
  18. package/build/types/index.d.ts.map +1 -1
  19. package/build/types/title/Title.d.ts +2 -2
  20. package/build/types/upload/steps/uploadImageStep/uploadImageStep.d.ts +1 -1
  21. package/build/types/uploadInput/uploadItem/UploadItemLink.d.ts +1 -1
  22. package/package.json +3 -3
  23. package/src/chips/Chip.tsx +4 -4
  24. package/src/chips/Chips.tsx +13 -18
  25. package/src/index.ts +0 -2
  26. package/src/main.css +0 -35
  27. package/src/main.less +0 -1
  28. package/build/markup/Markup.js +0 -128
  29. package/build/markup/Markup.js.map +0 -1
  30. package/build/markup/Markup.mjs +0 -126
  31. package/build/markup/Markup.mjs.map +0 -1
  32. package/build/markup/constants.js +0 -36
  33. package/build/markup/constants.js.map +0 -1
  34. package/build/markup/constants.mjs +0 -29
  35. package/build/markup/constants.mjs.map +0 -1
  36. package/build/markup/utils/parseMarkup.js +0 -226
  37. package/build/markup/utils/parseMarkup.js.map +0 -1
  38. package/build/markup/utils/parseMarkup.mjs +0 -223
  39. package/build/markup/utils/parseMarkup.mjs.map +0 -1
  40. package/build/markup/utils/sanitise.js +0 -91
  41. package/build/markup/utils/sanitise.js.map +0 -1
  42. package/build/markup/utils/sanitise.mjs +0 -89
  43. package/build/markup/utils/sanitise.mjs.map +0 -1
  44. package/build/styles/markup/Markup.css +0 -28
  45. package/build/styles/markup/_stories/Vulnerability/Vulnerability.css +0 -70
  46. package/build/types/markup/Markup.d.ts +0 -4
  47. package/build/types/markup/Markup.d.ts.map +0 -1
  48. package/build/types/markup/Markup.types.d.ts +0 -63
  49. package/build/types/markup/Markup.types.d.ts.map +0 -1
  50. package/build/types/markup/_stories/Vulnerability/Vulnerability.d.ts +0 -12
  51. package/build/types/markup/_stories/Vulnerability/Vulnerability.d.ts.map +0 -1
  52. package/build/types/markup/constants.d.ts +0 -20
  53. package/build/types/markup/constants.d.ts.map +0 -1
  54. package/build/types/markup/index.d.ts +0 -3
  55. package/build/types/markup/index.d.ts.map +0 -1
  56. package/build/types/markup/utils/parseMarkup.d.ts +0 -6
  57. package/build/types/markup/utils/parseMarkup.d.ts.map +0 -1
  58. package/build/types/markup/utils/sanitise.d.ts +0 -2
  59. package/build/types/markup/utils/sanitise.d.ts.map +0 -1
  60. package/src/markup/Markup.css +0 -28
  61. package/src/markup/Markup.less +0 -29
  62. package/src/markup/Markup.test.tsx +0 -330
  63. package/src/markup/Markup.tsx +0 -144
  64. package/src/markup/Markup.types.ts +0 -48
  65. package/src/markup/_stories/Markup.docs.mdx +0 -75
  66. package/src/markup/_stories/Markup.security.docs.mdx +0 -256
  67. package/src/markup/_stories/Markup.story.tsx +0 -198
  68. package/src/markup/_stories/Vulnerability/Vulnerability.css +0 -70
  69. package/src/markup/_stories/Vulnerability/Vulnerability.tsx +0 -30
  70. package/src/markup/constants.ts +0 -41
  71. package/src/markup/index.ts +0 -2
  72. package/src/markup/utils/parseMarkup.test.ts +0 -658
  73. package/src/markup/utils/parseMarkup.ts +0 -228
  74. package/src/markup/utils/sanitise.test.ts +0 -375
  75. package/src/markup/utils/sanitise.ts +0 -107
@@ -1,75 +0,0 @@
1
- import { Meta, Source } from '@storybook/addon-docs/blocks';
2
-
3
- <Meta title="Typography/Markup/Developer notes" />
4
-
5
- # Developer notes
6
-
7
- ## How parsing works
8
-
9
- The parser runs in two phases: **tokenise** then **parse**.
10
-
11
- ### Phase 1: Tokenise
12
-
13
- The input string is scanned left-to-right with a regex that only recognises allowed tag names. Everything between matches becomes text. Newlines become their own tokens.
14
-
15
- <Source dark language="text" code={`
16
- Input: "Hello <important>world</important>!\\nBye"
17
-
18
- Tokens: [ TEXT:"Hello " ]
19
- [ OPEN:important ]
20
- [ TEXT:"world" ]
21
- [ CLOSE:important ]
22
- [ TEXT:"!" ]
23
- [ NEWLINE ]
24
- [ TEXT:"Bye" ]
25
- `} />
26
-
27
- Unrecognised tags like `<script>` or `<div>` are never tokenised — they stay as part of the text content. This is the first security layer.
28
-
29
- ### Phase 2: Recursive descent parse
30
-
31
- The token stream is consumed by a recursive parser that builds a tree of nodes.
32
-
33
- <Source
34
- dark
35
- language="text"
36
- code={`
37
- parse(tokens, until=none)
38
- │
39
- ├─ TEXT:"Hello " ────────────────────► { type: 'text', content: 'Hello ' }
40
- ├─ OPEN:important
41
- │ │
42
- │ ├─ parse(tokens, until='important') // recurse
43
- │ │ ├─ TEXT:"world" ───────────────► { type: 'text', content: 'world' }
44
- │ │ └─ CLOSE:important ────────────► return { closed: true, nodes: [...] }
45
- │ │
46
- │ └─ result.closed? YES ────────────► { type: 'important', children: [...] }
47
- │
48
- ├─ TEXT:"!" ─────────────────────────► { type: 'text', content: '!' }
49
- ├─ NEWLINE ──────────────────────────► { type: 'newline' }
50
- └─ TEXT:"Bye" ───────────────────────► { type: 'text', content: 'Bye' }
51
- `}
52
- />
53
-
54
- The key insight is the `closed` flag. When we see an open tag, we recurse to collect its children. Two outcomes:
55
-
56
- 1. **Matching close tag found:** build a proper node with children.
57
- 2. **No matching close tag found:** treat the open tag as literal text, and return the already-parsed children.
58
-
59
- This gives graceful degradation for malformed input — no crashes, no swallowed content.
60
-
61
- ### Nesting
62
-
63
- Because each open tag recurses, nesting is handled naturally:
64
-
65
- <Source dark language="text" code={`
66
- Input: "<p><important>Read the <a href="/terms">terms</a></important></p>"
67
-
68
- Tree: paragraph
69
- └─ important
70
- ├─ text: "Read the "
71
- └─ link (href="/terms")
72
- └─ text: "terms"
73
- `} />
74
-
75
- The parser enforces a `MAX_NESTING_DEPTH` limit (32). Beyond that depth, tags are emitted as literal text instead of recursing — this prevents stack overflow from deeply-nested input.
@@ -1,256 +0,0 @@
1
- import { Meta } from '@storybook/addon-docs/blocks';
2
- import { MAX_NESTING_DEPTH } from '../utils/parseMarkup';
3
- import { Vulnerability, Badge } from './Vulnerability/Vulnerability';
4
-
5
- <Meta title="Typography/Markup/Parser security" />
6
-
7
- # Parser security
8
-
9
- ## Security model
10
-
11
- Three layers, each independent:
12
-
13
- 1. Tag allow-list (tokeniser). Only known tags are tokenised. `<script>`, `<img>`, `<iframe>`, etc. stay as
14
- literal text.
15
- 2. Attribute allow-list (parseAttributes). Only `href`, `action`, `target`, `accessibilityLabel` are extracted. `onclick`, `onerror`, etc. are silently ignored.
16
- 3. Protocol allow-list (sanitise). Only `https:`, `mailto:`, `/path`, and `#fragment` are allowed. Relative paths (`./`, `../`), `javascript:`, `data:`, `//`, and all other schemes are blocked.
17
-
18
- No `dangerouslySetInnerHTML` is used anywhere. The parser produces a typed AST, and the renderer maps it to React elements — React's own escaping handles the rest.
19
-
20
- ## Attack vector
21
-
22
- This page documents all attack vectors tested against the Markup component. Each section shows the raw malicious input and how the component neutralises it.
23
-
24
- ### XSS: Dangerous protocols
25
-
26
- #### javascript: protocol <Badge severity="High" />
27
-
28
- The `javascript:` protocol is blocked by the href sanitiser. The link degrades to plain text.
29
-
30
- <Vulnerability markup='<link href="javascript:alert(1)">click me</link>' />
31
-
32
- #### javascript: mixed case <Badge severity="High" />
33
-
34
- `new URL()` normalises the protocol to lowercase before checking the allowlist.
35
-
36
- <Vulnerability markup='<link href="JavaScript:alert(1)">mixed case</link>' />
37
-
38
- #### data: URI with script <Badge severity="High" />
39
-
40
- The `data:` protocol is blocked regardless of MIME type or encoding.
41
-
42
- <Vulnerability markup='<link href="data:text/html,<script>alert(1)</script>">data URI</link>' />
43
-
44
- #### data: base64-encoded <Badge severity="High" />
45
-
46
- Base64-encoded payloads are equally blocked.
47
-
48
- <Vulnerability markup='<link href="data:text/html;base64,PHNjcmlwdD5hbGVydCgxKTwvc2NyaXB0Pg==">base64</link>' />
49
-
50
- #### vbscript: protocol <Badge severity="High" />
51
-
52
- Legacy protocol explicitly blocked.
53
-
54
- <Vulnerability markup='<link href="vbscript:MsgBox(1)">vbscript</link>' />
55
-
56
- #### SVG data URI in href <Badge severity="High" />
57
-
58
- SVG data URIs can contain executable JavaScript. Blocked by the `data:` protocol check.
59
-
60
- <Vulnerability markup='<link href="data:image/svg+xml,<svg onload=alert(1)>">svg data</link>' />
61
-
62
- ### XSS: Control character bypass
63
-
64
- #### Null byte in scheme <Badge severity="High" />
65
-
66
- Null bytes inserted mid-scheme cause URL parsing to fail. The sanitiser rejects any href containing control characters.
67
-
68
- <Vulnerability markup={'<link href="java\x00script:alert(1)">null byte</link>'} />
69
-
70
- #### Zero-width space prefix <Badge severity="High" />
71
-
72
- Invisible characters (ZWSP, BOM, ZWNJ) are stripped before validation so they cannot bypass `.trim()`.
73
-
74
- <Vulnerability markup={'<link href="​javascript:alert(1)">invisible prefix</link>'} />
75
-
76
- #### URL-encoded colon (%3A) <Badge severity="Medium" />
77
-
78
- The sanitiser checks for `javascript%3a` patterns that evade URL constructor parsing.
79
-
80
- <Vulnerability markup='<link href="javascript%3Aalert(1)">encoded colon</link>' />
81
-
82
- ### XSS: HTML tag injection
83
-
84
- #### &lt;script&gt; tag <Badge severity="High" />
85
-
86
- Not in the allowed tag list. Never tokenised — remains as escaped text.
87
-
88
- <Vulnerability markup='<script>alert("xss")</script>' />
89
-
90
- #### &lt;img onerror&gt; <Badge severity="High" />
91
-
92
- Not in the allowed tag list. Passes through as literal text rendered via `textContent`.
93
-
94
- <Vulnerability markup="<img src=x onerror=alert(1)>" />
95
-
96
- #### &lt;svg onload&gt; <Badge severity="High" />
97
-
98
- Not in the allowed tag list. Rendered as harmless text content.
99
-
100
- <Vulnerability markup="<svg onload=alert(1)>" />
101
-
102
- #### &lt;svg&gt; inside allowed element <Badge severity="High" />
103
-
104
- Even inside a valid element, unrecognised tags are text nodes. React escapes them.
105
-
106
- <Vulnerability markup="<important><svg onload=alert(1)></important>" />
107
-
108
- #### &lt;iframe&gt; injection <Badge severity="High" />
109
-
110
- Not in the allowed tag list. The entire string renders as escaped text.
111
-
112
- <Vulnerability markup='<iframe src="javascript:alert(1)"></iframe>' />
113
-
114
- #### &lt;img&gt; inside link <Badge severity="High" />
115
-
116
- Unrecognised tags within link content are safe text children.
117
-
118
- <Vulnerability markup='<link href="/safe"><img src=x onerror=alert(1)></link>' />
119
-
120
- #### &lt;input&gt; with autofocus <Badge severity="High" />
121
-
122
- Not in the allowed tag list. Cannot trigger focus-based XSS.
123
-
124
- <Vulnerability markup="<input onfocus=alert(1) autofocus>" />
125
-
126
- #### &lt;details ontoggle&gt; <Badge severity="High" />
127
-
128
- Not in the allowed tag list. Cannot trigger toggle-based XSS.
129
-
130
- <Vulnerability markup="<details open ontoggle=alert(1)>" />
131
-
132
- ### XSS: Attribute injection
133
-
134
- #### onclick attribute on link <Badge severity="High" />
135
-
136
- Only `href`, `action`, `target`, and `accessibilityLabel` are extracted. All event handlers are discarded.
137
-
138
- <Vulnerability markup='<link href="/safe" onclick="alert(1)">click</link>' />
139
-
140
- #### HTML in accessibilityLabel <Badge severity="Medium" />
141
-
142
- HTML metacharacters are stripped from `accessibilityLabel` to prevent injection if the value is ever used in an unsafe context.
143
-
144
- <Vulnerability markup='<important accessibilityLabel="&lt;img onerror=alert(1)&gt;">safe text</important>' />
145
-
146
- #### Slash as attribute separator <Badge severity="Low" />
147
-
148
- The attribute regex now requires whitespace before attribute names. `<link/href="x">` no longer parses the href.
149
-
150
- <Vulnerability markup='<link/href="/evil">slash separator</link>' />
151
-
152
- ### Open redirect
153
-
154
- #### Protocol-relative URL (//) <Badge severity="High" />
155
-
156
- Protocol-relative URLs navigate to external domains. Blocked by `startsWith("//")` check.
157
-
158
- <Vulnerability markup='<link href="//evil.com/phishing">protocol relative</link>' />
159
-
160
- #### Backslash after slash (/\) <Badge severity="High" />
161
-
162
- Browsers normalise `\` to `/`, making `/\evil.com` equivalent to `//evil.com`. Blocked.
163
-
164
- <Vulnerability markup='<link href="/\evil.com">backslash redirect</link>' />
165
-
166
- #### Relative path traversal <Badge severity="Medium" />
167
-
168
- Relative paths (`./`, `../`) could be exploited to navigate outside the expected domain or bypass path-based security checks. Only absolute `https:` URLs, `mailto:`, root-relative `/path`, and `#fragment` are allowed.
169
-
170
- <Vulnerability markup='<link href="../admin/secret">parent traversal</link>' />
171
-
172
- <Vulnerability markup='<link href="./evil-page">dot relative</link>' />
173
-
174
- #### Credential confusion (@) <Badge severity="Medium" />
175
-
176
- In `https://good.com@evil.com`, the actual host is `evil.com`. URLs with username/password components are rejected.
177
-
178
- <Vulnerability markup='<link href="https://good.com@evil.com">phishing link</link>' />
179
-
180
- ### SSRF: Internal network
181
-
182
- #### Localhost IP <Badge severity="Medium" />
183
-
184
- Numeric IPs, hex IPs, localhost, and `*.internal` hosts are blocked.
185
-
186
- <Vulnerability markup='<link href="https://127.0.0.1/admin">localhost</link>' />
187
-
188
- #### Cloud metadata endpoint <Badge severity="Medium" />
189
-
190
- Prevents access to cloud instance metadata services.
191
-
192
- <Vulnerability markup='<link href="https://169.254.169.254/latest/meta-data">AWS metadata</link>' />
193
-
194
- #### IPv6 localhost <Badge severity="Medium" />
195
-
196
- IPv6 loopback address blocked.
197
-
198
- <Vulnerability markup='<link href="https://[::1]/">ipv6 localhost</link>' />
199
-
200
- ### DoS: Stack overflow
201
-
202
- #### Deep nesting <Badge severity="High" />
203
-
204
- The recursive parser enforces a `MAX_NESTING_DEPTH` limit (currently {MAX_NESTING_DEPTH}). Beyond that, tags are emitted as literal text.
205
-
206
- <Vulnerability
207
- markup={
208
- '<important>'.repeat(MAX_NESTING_DEPTH + 20) +
209
- 'deep' +
210
- '</important>'.repeat(MAX_NESTING_DEPTH + 20)
211
- }
212
- />
213
-
214
- ### Prototype pollution
215
-
216
- #### action="\_\_proto\_\_" <Badge severity="Medium" />
217
-
218
- Action values that collide with `Object.prototype` properties are rejected by the parser.
219
- Without this, `actions?.['__proto__']` resolves to a truthy inherited value, rendering an unintended clickable link.
220
-
221
- <Vulnerability markup='<link action="__proto__">click</link>' />
222
-
223
- #### action="constructor" <Badge severity="Medium" />
224
-
225
- `Object.prototype.constructor` is the `Object` function — truthy and callable. Blocked.
226
-
227
- <Vulnerability markup='<link action="constructor">click</link>' />
228
-
229
- ### Attribute injection
230
-
231
- #### Duplicate href (first wins) <Badge severity="Medium" />
232
-
233
- When the same attribute appears twice, the first value is used. Previously the last won,
234
- which could bypass content filters that only inspect the first occurrence.
235
-
236
- <Vulnerability markup='<link href="/safe" href="javascript:alert(1)">duplicate href</link>' />
237
-
238
- #### target="\_top" (iframe breakout) <Badge severity="Low" />
239
-
240
- The `target` attribute is restricted to `_blank` and `_self`. Values like `_top` and `_parent` are stripped.
241
-
242
- <Vulnerability markup='<link href="https://example.com" target="_top">escape iframe</link>' />
243
-
244
- ### Mailto abuse
245
-
246
- #### Header injection (cc/bcc) <Badge severity="Medium" />
247
-
248
- Mailto query parameters can inject additional recipients. Mailto URLs with query strings are blocked.
249
-
250
- <Vulnerability markup='<link href="mailto:evil@evil.com?cc=victim@good.com&bcc=spy@evil.com">contact</link>' />
251
-
252
- #### No recipient (social engineering) <Badge severity="Medium" />
253
-
254
- Mailto URLs must have an actual address.
255
-
256
- <Vulnerability markup='<link href="mailto:?subject=urgent&body=click%20here">empty mailto</link>' />
@@ -1,198 +0,0 @@
1
- import React, { useState } from 'react';
2
- import { Meta, StoryObj } from '@storybook/react-webpack5';
3
- import { action } from 'storybook/actions';
4
-
5
- import {
6
- createSandboxStory,
7
- globalScope,
8
- } from '../../../.storybook/components/sandbox/SandboxEditor';
9
- import { storySourceWithoutNoise } from '../../../.storybook/helpers';
10
-
11
- import { Markup } from '../Markup';
12
- import type { MarkupActionHandler } from '../Markup.types';
13
-
14
- export default {
15
- component: Markup,
16
- title: 'Typography/Markup',
17
- tags: ['early-access'],
18
- } satisfies Meta<typeof Markup>;
19
-
20
- type Story = StoryObj<typeof Markup>;
21
-
22
- const MARKUP_STRING = `This markup string supports number of tags:
23
- <important>important</important>,
24
- <positive>positive</positive>,
25
- <negative>negative</negative>,
26
- <strong>strong</strong>,
27
- <strikethrough accessibilityLabel="old value label">strikethrough with a screen-reader-friendly label</strikethrough>,
28
- and a <link href="https://docs.wise.design">link</link>.
29
- <paragraph>
30
- It respects
31
- line breaks
32
- and also allows for paragraphs <positive>and nest tags</positive>.
33
- </paragraph>
34
- `;
35
-
36
- export const Playground: Story = {
37
- args: {
38
- children: MARKUP_STRING,
39
- },
40
- };
41
-
42
- export const Sandbox = createSandboxStory({
43
- code: `<Markup>
44
- {\`${MARKUP_STRING}\`}
45
- </Markup>`,
46
- scope: globalScope,
47
- });
48
-
49
- /**
50
- * Renders the design system `Link` component inline. `<a>` is an alias for `<link>`. External
51
- * links with `target="_blank"` automatically receive a navigate-away icon and `rel="noreferrer"`.
52
- * */
53
- export const WithLinks: Story = {
54
- args: {
55
- children:
56
- 'Using link tag: <link href="/settings">settings</link>. Using a tag: <a href="https://wise.com" target="_blank">our website</a>.',
57
- },
58
- };
59
-
60
- /**
61
- * Button-mode link using the `action` attribute. The handler is provided via the `actions` prop.
62
- * If the named action is missing from the map, the link degrades to plain text.
63
- * */
64
- export const WithLinksAsButtons = storySourceWithoutNoise<Story>({
65
- args: {
66
- children: 'Click <link action="increment">increment</link> to add `1`.',
67
- },
68
- render: function Render(args) {
69
- const [count, setCount] = useState(0);
70
-
71
- const increment: MarkupActionHandler = (event) => {
72
- action('Button event')(event);
73
- setCount((prev) => prev + 1);
74
- };
75
-
76
- return (
77
- <>
78
- <Markup actions={{ increment }}>{args.children}</Markup>
79
- <output>{count}</output>
80
- </>
81
- );
82
- },
83
- });
84
-
85
- /**
86
- * `<strong>` renders a `<strong>` element. `<strikethrough>` renders a `<del>` element for
87
- * struck-through text.
88
- * */
89
- export const StrongAndStrikethrough: Story = {
90
- args: {
91
- children:
92
- 'This is <strong>bold</strong> and this is <strikethrough>no longer relevant</strikethrough>.',
93
- },
94
- };
95
-
96
- /**
97
- * Block-level paragraphs. `<p>` is an alias for `<paragraph>`. Both render a `<p>` element.
98
- * */
99
- export const Paragraphs: Story = {
100
- args: {
101
- children:
102
- '<paragraph>First paragraph using the full tag name.</paragraph><p>Second paragraph using the short alias.</p>',
103
- },
104
- };
105
-
106
- /**
107
- * Tags can nest to arbitrary depth — emphasis wrapping a link, strong inside a paragraph, etc.
108
- * */
109
- export const Nested: Story = {
110
- args: {
111
- children:
112
- '<p><important>Read the <a href="/terms">terms and conditions</a> carefully</important></p>',
113
- },
114
- };
115
-
116
- /**
117
- * Literal newline characters (`\n`) in the string are converted to `<br />` elements.
118
- * */
119
- export const Newlines: Story = {
120
- args: {
121
- children: 'First line.\nSecond line.\nThird line.',
122
- },
123
- };
124
-
125
- /**
126
- * Non-string children (JSX, components, numbers) pass through unchanged — no parsing occurs.
127
- * This lets parent components use Markup as a unified slot for both markup strings and JSX.
128
- * */
129
- export const PassThrough: Story = {
130
- render: () => (
131
- <Markup>
132
- <em>
133
- This JSX is <strong>passed through</strong>
134
- </em>{' '}
135
- without parsing.
136
- </Markup>
137
- ),
138
- };
139
-
140
- /**
141
- * The wrapper element is automatically chosen based on content: `<span>` for inline-only markup,
142
- * `<div>` when `<paragraph>` nodes are present (to avoid invalid `<p>` inside `<span>`).
143
- * The `as` prop overrides auto-detection when you need explicit control.
144
- * */
145
- export const WrapperElement: Story = {
146
- parameters: {
147
- docs: {
148
- source: { type: 'dynamic' },
149
- },
150
- },
151
- render: () => (
152
- <>
153
- <Markup className="demo-wrapper">
154
- {'This is <important>inline</important> content — wrapper is a span.'}
155
- </Markup>
156
- <Markup className="demo-wrapper">
157
- {'<p>Block content with a paragraph.</p><p>Second paragraph.</p>'}
158
- </Markup>
159
- <Markup as="section" className="demo-wrapper">
160
- {'<p>Paragraph forced into a span wrapper via the as prop.</p>'}
161
- </Markup>
162
- </>
163
- ),
164
- };
165
-
166
- /**
167
- * Not all semantic tags are announced by screen readers — for example, `<del>` (strikethrough) is
168
- * often skipped. The `accessibilityLabel` attribute provides extended copy for visually impaired
169
- * users, ensuring the meaning is conveyed even when the visual styling is not perceived. It is
170
- * rendered as `aria-label` on the DOM element.
171
- * */
172
- export const AccessibilityLabels: Story = {
173
- args: {
174
- children: [
175
- 'Was <strikethrough accessibilityLabel="Previously 50 GBP">£50.00</strikethrough>,',
176
- 'now <strong accessibilityLabel="New price">£35.00</strong>.',
177
- '<important accessibilityLabel="Offer ending soon">Limited time offer</important>',
178
- ].join(' '),
179
- },
180
- };
181
-
182
- /**
183
- * The component applies a strict whitelist of allowed tags and attributes. Malicious or malformed input is rendered as literal text, preventing XSS attacks. [Full list of handled attack vectors](?path=/docs/typography-markup-parser-security--docs)
184
- * */
185
- export const MalformedInput: Story = {
186
- args: {
187
- children: [
188
- '<script>alert("xss")</script>',
189
- '<img src=x onerror="alert(1)">',
190
- '<iframe src="https://evil.com"></iframe>',
191
- '<link href="javascript:alert(1)">blocked protocol</link>',
192
- '<link href="data:text/html,<script>alert(1)</script>">data URI</link>',
193
- '<a href="vbscript:MsgBox">vbscript</a>',
194
- '<link onclick="steal()" href="/safe">ignored attribute</link>',
195
- '<important>this is safe</important>',
196
- ].join('\n'),
197
- },
198
- };
@@ -1,70 +0,0 @@
1
- .vulnerability {
2
- display: grid;
3
- grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
4
- gap: 4px 16px;
5
- margin-bottom: 32px;
6
- }
7
-
8
- .vulnerability__label {
9
- font-size: 11px;
10
- font-weight: 600;
11
- color: #64748b;
12
- text-transform: uppercase;
13
- }
14
-
15
- .vulnerability__input {
16
- font-family: ui-monospace, SFMono-Regular, Consolas, 'Liberation Mono', Menlo, monospace;
17
- width: 100%;
18
- min-height: 56px;
19
- padding: 10px;
20
- font-size: 12px;
21
- border-radius: 4px;
22
- border: 1px solid #e2e8f0;
23
- background: #000000d9;
24
- color: #ededed;
25
- field-sizing: content;
26
- resize: none;
27
- overflow-wrap: break-word;
28
- }
29
-
30
- .vulnerability__output {
31
- font-family: ui-monospace, SFMono-Regular, Consolas, 'Liberation Mono', Menlo, monospace;
32
- padding: 10px;
33
- min-height: 56px;
34
- border-radius: 4px;
35
- border: 1px solid #e2e8f0;
36
- background: #ffffff;
37
- font-size: 14px;
38
- line-height: 1.5;
39
- overflow-wrap: break-word;
40
- }
41
-
42
- .vulnerability__badge {
43
- display: inline-block;
44
- padding: 3px 8px;
45
- border-radius: 3em;
46
- font-size: 11px;
47
- line-height: 0.75rem;
48
- font-weight: 700;
49
- margin-left: 8px;
50
- vertical-align: middle;
51
- text-align: center;
52
- background-color: var(--badge-bg);
53
- color: var(--badge-color);
54
- box-shadow: color-mix(in srgb, var(--badge-color) 10%, transparent) 0 0 0 1px inset;
55
- }
56
-
57
- .vulnerability__badge--high {
58
- --badge-bg: #ffe8ea;
59
- --badge-color: #f30010;
60
- }
61
-
62
- .vulnerability__badge--medium {
63
- --badge-bg: #fff7cf;
64
- --badge-color: #51390d;
65
- }
66
-
67
- .vulnerability__badge--low {
68
- --badge-bg: #d7f8ce;
69
- --badge-color: #00511e;
70
- }
@@ -1,30 +0,0 @@
1
- import { Markup } from '../../Markup';
2
-
3
- import './Vulnerability.css';
4
-
5
- type Severity = 'High' | 'Medium' | 'Low';
6
-
7
- interface VulnerabilityProps {
8
- markup: string;
9
- }
10
-
11
- export const Vulnerability = ({ markup }: VulnerabilityProps) => (
12
- <div className="vulnerability">
13
- <div className="vulnerability__label">Markup input</div>
14
- <div className="vulnerability__label">Rendered output</div>
15
- <textarea readOnly value={markup} className="vulnerability__input" aria-label="Markup input" />
16
- <div className="vulnerability__output">
17
- <Markup>{markup}</Markup>
18
- </div>
19
- </div>
20
- );
21
-
22
- interface BadgeProps {
23
- severity: Severity;
24
- }
25
-
26
- export const Badge = ({ severity }: BadgeProps) => (
27
- <span className={`vulnerability__badge vulnerability__badge--${severity.toLowerCase()}`}>
28
- {severity}
29
- </span>
30
- );
@@ -1,41 +0,0 @@
1
- // Only these tags are recognised — anything else is treated as plain text (security allow-list)
2
- export const ALLOWED_TAGS = [
3
- 'important',
4
- 'positive',
5
- 'negative',
6
- 'strong',
7
- 'strikethrough',
8
- 'paragraph',
9
- 'p',
10
- 'link',
11
- 'a',
12
- ] as const;
13
-
14
- /**
15
- * Matches opening and closing tags from the ALLOWED_TAGS allow-list.
16
- *
17
- * Capture groups:
18
- * 1. `/` if this is a closing tag, undefined otherwise
19
- * 2. Tag name
20
- * 3. Raw attribute string between the tag name and `>` (empty for closing tags)
21
- *
22
- * The lookahead `(?=[\s>/])` after the tag name ensures we don't match partial
23
- * words — e.g. `<stronger>` won't be matched as `<strong>`.
24
- *
25
- * Global flag (`g`) allows iterative matching via `exec()`.
26
- */
27
- export const TAG_REGEX = new RegExp(
28
- `<(\\/)?(${ALLOWED_TAGS.join('|')})(?=[\\s>/])((?:[^>"']*(?:"[^"]*"|'[^']*'))*[^>"']*)>`,
29
- 'g',
30
- );
31
-
32
- export const TAG_ALIASES: Record<string, string> = { p: 'paragraph', a: 'link' };
33
-
34
- // Maximum recursive nesting before the parser emits tags as literal text (DoS protection)
35
- export const MAX_NESTING_DEPTH = 32;
36
-
37
- // Only these link targets are allowed — others are stripped to prevent iframe breakout
38
- export const ALLOWED_TARGETS = ['_blank', '_self'];
39
-
40
- // Only these protocols pass href validation — all others are blocked
41
- export const ALLOWED_PROTOCOLS = ['https:', 'mailto:'];
@@ -1,2 +0,0 @@
1
- export { Markup } from './Markup';
2
- export type { MarkupActionHandler, MarkupActions, MarkupNode, MarkupProps } from './Markup.types';