@transferwise/components 0.0.0-experimental-476a8e9 → 0.0.0-experimental-d23525e
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/build/i18n/ro.json +1 -1
- package/build/i18n/ro.json.js +1 -1
- package/build/i18n/ro.json.mjs +1 -1
- package/build/index.js +0 -2
- package/build/index.js.map +1 -1
- package/build/index.mjs +0 -1
- package/build/index.mjs.map +1 -1
- package/build/main.css +0 -35
- package/build/styles/main.css +0 -35
- package/build/types/body/Body.d.ts +2 -2
- package/build/types/iconButton/IconButton.d.ts +1 -1
- package/build/types/index.d.ts +1 -3
- package/build/types/index.d.ts.map +1 -1
- package/build/types/title/Title.d.ts +2 -2
- package/build/types/upload/steps/uploadImageStep/uploadImageStep.d.ts +1 -1
- package/build/types/uploadInput/uploadItem/UploadItemLink.d.ts +1 -1
- package/package.json +1 -1
- package/src/i18n/ro.json +1 -1
- package/src/index.ts +2 -2
- package/src/main.css +0 -35
- package/src/main.less +0 -1
- package/build/markup/Markup.js +0 -125
- package/build/markup/Markup.js.map +0 -1
- package/build/markup/Markup.mjs +0 -123
- package/build/markup/Markup.mjs.map +0 -1
- package/build/markup/constants.js +0 -36
- package/build/markup/constants.js.map +0 -1
- package/build/markup/constants.mjs +0 -29
- package/build/markup/constants.mjs.map +0 -1
- package/build/markup/utils/parseMarkup.js +0 -226
- package/build/markup/utils/parseMarkup.js.map +0 -1
- package/build/markup/utils/parseMarkup.mjs +0 -223
- package/build/markup/utils/parseMarkup.mjs.map +0 -1
- package/build/markup/utils/sanitise.js +0 -91
- package/build/markup/utils/sanitise.js.map +0 -1
- package/build/markup/utils/sanitise.mjs +0 -89
- package/build/markup/utils/sanitise.mjs.map +0 -1
- package/build/styles/markup/Markup.css +0 -28
- package/build/styles/markup/_stories/Vulnerability/Vulnerability.css +0 -70
- package/build/types/markup/Markup.d.ts +0 -4
- package/build/types/markup/Markup.d.ts.map +0 -1
- package/build/types/markup/Markup.types.d.ts +0 -58
- package/build/types/markup/Markup.types.d.ts.map +0 -1
- package/build/types/markup/_stories/Vulnerability/Vulnerability.d.ts +0 -12
- package/build/types/markup/_stories/Vulnerability/Vulnerability.d.ts.map +0 -1
- package/build/types/markup/constants.d.ts +0 -20
- package/build/types/markup/constants.d.ts.map +0 -1
- package/build/types/markup/index.d.ts +0 -3
- package/build/types/markup/index.d.ts.map +0 -1
- package/build/types/markup/utils/parseMarkup.d.ts +0 -6
- package/build/types/markup/utils/parseMarkup.d.ts.map +0 -1
- package/build/types/markup/utils/sanitise.d.ts +0 -2
- package/build/types/markup/utils/sanitise.d.ts.map +0 -1
- package/src/markup/Markup.css +0 -28
- package/src/markup/Markup.less +0 -29
- package/src/markup/Markup.test.tsx +0 -296
- package/src/markup/Markup.tsx +0 -130
- package/src/markup/Markup.types.ts +0 -43
- package/src/markup/_stories/Markup.docs.mdx +0 -75
- package/src/markup/_stories/Markup.security.docs.mdx +0 -256
- package/src/markup/_stories/Markup.story.tsx +0 -202
- package/src/markup/_stories/Vulnerability/Vulnerability.css +0 -70
- package/src/markup/_stories/Vulnerability/Vulnerability.tsx +0 -30
- package/src/markup/constants.ts +0 -41
- package/src/markup/index.ts +0 -2
- package/src/markup/utils/parseMarkup.test.ts +0 -658
- package/src/markup/utils/parseMarkup.ts +0 -228
- package/src/markup/utils/sanitise.test.ts +0 -375
- package/src/markup/utils/sanitise.ts +0 -107
|
@@ -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
|
-
#### <script> 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
|
-
#### <img onerror> <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
|
-
#### <svg onload> <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
|
-
#### <svg> 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
|
-
#### <iframe> 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
|
-
#### <img> 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
|
-
#### <input> 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
|
-
#### <details ontoggle> <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="<img onerror=alert(1)>">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,202 +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
|
-
render: () => (
|
|
147
|
-
<div style={{ display: 'flex', flexDirection: 'column', gap: 16 }}>
|
|
148
|
-
<div>
|
|
149
|
-
<strong>Default (inline content → span):</strong>
|
|
150
|
-
<Markup className="demo-wrapper">
|
|
151
|
-
{'This is <important>inline</important> content — wrapper is a span.'}
|
|
152
|
-
</Markup>
|
|
153
|
-
</div>
|
|
154
|
-
<div>
|
|
155
|
-
<strong>Default (paragraph content → div):</strong>
|
|
156
|
-
<Markup className="demo-wrapper">
|
|
157
|
-
{'<p>Block content with a paragraph.</p><p>Second paragraph.</p>'}
|
|
158
|
-
</Markup>
|
|
159
|
-
</div>
|
|
160
|
-
<div>
|
|
161
|
-
<strong>Explicit as="span" override:</strong>
|
|
162
|
-
<Markup as="span" className="demo-wrapper">
|
|
163
|
-
{'<p>Paragraph forced into a span wrapper via the as prop.</p>'}
|
|
164
|
-
</Markup>
|
|
165
|
-
</div>
|
|
166
|
-
</div>
|
|
167
|
-
),
|
|
168
|
-
};
|
|
169
|
-
|
|
170
|
-
/**
|
|
171
|
-
* Not all semantic tags are announced by screen readers — for example, `<del>` (strikethrough) is
|
|
172
|
-
* often skipped. The `accessibilityLabel` attribute provides extended copy for visually impaired
|
|
173
|
-
* users, ensuring the meaning is conveyed even when the visual styling is not perceived. It is
|
|
174
|
-
* rendered as `aria-label` on the DOM element.
|
|
175
|
-
* */
|
|
176
|
-
export const AccessibilityLabels: Story = {
|
|
177
|
-
args: {
|
|
178
|
-
children: [
|
|
179
|
-
'Was <strikethrough accessibilityLabel="Previously 50 GBP">£50.00</strikethrough>,',
|
|
180
|
-
'now <strong accessibilityLabel="New price">£35.00</strong>.',
|
|
181
|
-
'<important accessibilityLabel="Offer ending soon">Limited time offer</important>',
|
|
182
|
-
].join(' '),
|
|
183
|
-
},
|
|
184
|
-
};
|
|
185
|
-
|
|
186
|
-
/**
|
|
187
|
-
* 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)
|
|
188
|
-
* */
|
|
189
|
-
export const MalformedInput: Story = {
|
|
190
|
-
args: {
|
|
191
|
-
children: [
|
|
192
|
-
'<script>alert("xss")</script>',
|
|
193
|
-
'<img src=x onerror="alert(1)">',
|
|
194
|
-
'<iframe src="https://evil.com"></iframe>',
|
|
195
|
-
'<link href="javascript:alert(1)">blocked protocol</link>',
|
|
196
|
-
'<link href="data:text/html,<script>alert(1)</script>">data URI</link>',
|
|
197
|
-
'<a href="vbscript:MsgBox">vbscript</a>',
|
|
198
|
-
'<link onclick="steal()" href="/safe">ignored attribute</link>',
|
|
199
|
-
'<important>this is safe</important>',
|
|
200
|
-
].join('\n'),
|
|
201
|
-
},
|
|
202
|
-
};
|
|
@@ -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
|
-
);
|
package/src/markup/constants.ts
DELETED
|
@@ -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:'];
|
package/src/markup/index.ts
DELETED