@transferwise/components 0.0.0-experimental-bcea1dd → 0.0.0-experimental-1168b1c
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/Calendar/Calendar.js +47 -8
- package/build/Calendar/Calendar.js.map +1 -1
- package/build/Calendar/Calendar.messages.js +3 -0
- package/build/Calendar/Calendar.messages.js.map +1 -1
- package/build/Calendar/Calendar.messages.mjs +3 -0
- package/build/Calendar/Calendar.messages.mjs.map +1 -1
- package/build/Calendar/Calendar.mjs +48 -9
- package/build/Calendar/Calendar.mjs.map +1 -1
- package/build/Calendar/DayCalendar/DayCalendar.js +4 -0
- package/build/Calendar/DayCalendar/DayCalendar.js.map +1 -1
- package/build/Calendar/DayCalendar/DayCalendar.mjs +4 -0
- package/build/Calendar/DayCalendar/DayCalendar.mjs.map +1 -1
- package/build/Calendar/DayCalendar/Table/DayCalendarTable.js +30 -18
- package/build/Calendar/DayCalendar/Table/DayCalendarTable.js.map +1 -1
- package/build/Calendar/DayCalendar/Table/DayCalendarTable.mjs +30 -18
- package/build/Calendar/DayCalendar/Table/DayCalendarTable.mjs.map +1 -1
- package/build/Calendar/MonthCalendar/Table/MonthCalendarTable.js +1 -1
- package/build/Calendar/MonthCalendar/Table/MonthCalendarTable.js.map +1 -1
- package/build/Calendar/MonthCalendar/Table/MonthCalendarTable.mjs +1 -1
- package/build/Calendar/MonthCalendar/Table/MonthCalendarTable.mjs.map +1 -1
- package/build/Calendar/{tableLink → TableLink}/TableLink.js +23 -11
- package/build/Calendar/TableLink/TableLink.js.map +1 -0
- package/build/Calendar/{tableLink → TableLink}/TableLink.mjs +23 -11
- package/build/Calendar/TableLink/TableLink.mjs.map +1 -0
- package/build/Calendar/YearCalendar/Table/YearCalendarTable.js +1 -1
- package/build/Calendar/YearCalendar/Table/YearCalendarTable.js.map +1 -1
- package/build/Calendar/YearCalendar/Table/YearCalendarTable.mjs +1 -1
- package/build/Calendar/YearCalendar/Table/YearCalendarTable.mjs.map +1 -1
- package/build/DateLookup/DateLookup.js +14 -4
- package/build/DateLookup/DateLookup.js.map +1 -1
- package/build/DateLookup/DateLookup.mjs +14 -4
- package/build/DateLookup/DateLookup.mjs.map +1 -1
- package/build/common/DateUtils/hasDisabledDatesInMonth/hasDisabledDatesInMonth.js +33 -0
- package/build/common/DateUtils/hasDisabledDatesInMonth/hasDisabledDatesInMonth.js.map +1 -0
- package/build/common/DateUtils/hasDisabledDatesInMonth/hasDisabledDatesInMonth.mjs +31 -0
- package/build/common/DateUtils/hasDisabledDatesInMonth/hasDisabledDatesInMonth.mjs.map +1 -0
- package/build/common/DateUtils/isDateDisabled/isDateDisabled.js +40 -0
- package/build/common/DateUtils/isDateDisabled/isDateDisabled.js.map +1 -0
- package/build/common/DateUtils/isDateDisabled/isDateDisabled.mjs +38 -0
- package/build/common/DateUtils/isDateDisabled/isDateDisabled.mjs.map +1 -0
- package/build/i18n/en.json +1 -0
- package/build/i18n/en.json.js +1 -0
- package/build/i18n/en.json.js.map +1 -1
- package/build/i18n/en.json.mjs +1 -0
- package/build/i18n/en.json.mjs.map +1 -1
- package/build/index.js +2 -4
- package/build/index.js.map +1 -1
- package/build/index.mjs +1 -2
- package/build/index.mjs.map +1 -1
- package/build/main.css +114 -76
- package/build/styles/Calendar/Calendar.css +104 -35
- package/build/styles/Calendar/TableLink/TableLink.css +109 -0
- package/build/styles/main.css +114 -76
- package/build/types/Body/Body.d.ts +2 -2
- package/build/types/Calendar/Calendar.d.ts +4 -2
- package/build/types/Calendar/Calendar.d.ts.map +1 -1
- package/build/types/Calendar/Calendar.messages.d.ts +5 -0
- package/build/types/Calendar/Calendar.messages.d.ts.map +1 -1
- package/build/types/Calendar/Calendar.types.d.ts +19 -0
- package/build/types/Calendar/Calendar.types.d.ts.map +1 -0
- package/build/types/Calendar/DayCalendar/DayCalendar.d.ts +4 -1
- package/build/types/Calendar/DayCalendar/DayCalendar.d.ts.map +1 -1
- package/build/types/Calendar/DayCalendar/Table/DayCalendarTable.d.ts +3 -0
- package/build/types/Calendar/DayCalendar/Table/DayCalendarTable.d.ts.map +1 -1
- package/build/types/Calendar/{tableLink → TableLink}/TableLink.d.ts +2 -1
- package/build/types/Calendar/TableLink/TableLink.d.ts.map +1 -0
- package/build/types/Calendar/TableLink/index.d.ts.map +1 -0
- package/build/types/Calendar/index.d.ts +1 -0
- package/build/types/Calendar/index.d.ts.map +1 -1
- package/build/types/DateLookup/DateLookup.d.ts +5 -2
- package/build/types/DateLookup/DateLookup.d.ts.map +1 -1
- package/build/types/DateLookup/index.d.ts +1 -0
- package/build/types/DateLookup/index.d.ts.map +1 -1
- package/build/types/IconButton/IconButton.d.ts +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/build/types/common/DateUtils/hasDisabledDatesInMonth/hasDisabledDatesInMonth.d.ts +3 -0
- package/build/types/common/DateUtils/hasDisabledDatesInMonth/hasDisabledDatesInMonth.d.ts.map +1 -0
- package/build/types/common/DateUtils/index.d.ts +2 -0
- package/build/types/common/DateUtils/index.d.ts.map +1 -1
- package/build/types/common/DateUtils/isDateDisabled/isDateDisabled.d.ts +3 -0
- package/build/types/common/DateUtils/isDateDisabled/isDateDisabled.d.ts.map +1 -0
- package/build/types/index.d.ts +2 -4
- package/build/types/index.d.ts.map +1 -1
- package/build/types/test-utils/index.d.ts +2 -0
- package/build/types/test-utils/index.d.ts.map +1 -1
- package/package.json +3 -3
- package/src/Calendar/Calendar.css +104 -35
- package/src/Calendar/Calendar.disabledDates.test.tsx +197 -0
- package/src/Calendar/Calendar.less +5 -35
- package/src/Calendar/Calendar.messages.ts +5 -0
- package/src/Calendar/Calendar.test.tsx +9 -9
- package/src/Calendar/Calendar.tsx +61 -9
- package/src/Calendar/Calendar.types.ts +22 -0
- package/src/Calendar/DayCalendar/DayCalendar.tsx +17 -1
- package/src/Calendar/DayCalendar/Table/DayCalendarTable.tsx +42 -26
- package/src/Calendar/MonthCalendar/Table/MonthCalendarTable.tsx +1 -1
- package/src/Calendar/TableLink/TableLink.css +109 -0
- package/src/Calendar/TableLink/TableLink.less +60 -0
- package/src/Calendar/{tableLink → TableLink}/TableLink.tsx +24 -14
- package/src/Calendar/YearCalendar/Table/YearCalendarTable.tsx +1 -1
- package/src/Calendar/_stories/Calendar.accessibility.docs.mdx +112 -0
- package/src/Calendar/_stories/Calendar.story.tsx +55 -1
- package/src/Calendar/index.ts +7 -0
- package/src/DateLookup/DateLookup.disabledDates.test.tsx +120 -0
- package/src/DateLookup/DateLookup.test.tsx +1 -1
- package/src/DateLookup/DateLookup.tsx +11 -4
- package/src/DateLookup/_stories/DateLookup.accessibility.docs.mdx +100 -0
- package/src/DateLookup/_stories/DateLookup.story.tsx +59 -1
- package/src/DateLookup/index.ts +6 -0
- package/src/common/DateUtils/hasDisabledDatesInMonth/hasDisabledDatesInMonth.test.ts +98 -0
- package/src/common/DateUtils/hasDisabledDatesInMonth/hasDisabledDatesInMonth.ts +45 -0
- package/src/common/DateUtils/index.ts +2 -0
- package/src/common/DateUtils/isDateDisabled/isDateDisabled.test.ts +159 -0
- package/src/common/DateUtils/isDateDisabled/isDateDisabled.ts +59 -0
- package/src/i18n/en.json +1 -0
- package/src/index.ts +2 -4
- package/src/main.css +114 -76
- package/src/main.less +0 -1
- package/build/Calendar/tableLink/TableLink.js.map +0 -1
- package/build/Calendar/tableLink/TableLink.mjs.map +0 -1
- package/build/Markup/Markup.js +0 -136
- package/build/Markup/Markup.js.map +0 -1
- package/build/Markup/Markup.mjs +0 -131
- 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/Calendar/tableLink/TableLink.d.ts.map +0 -1
- package/build/types/Calendar/tableLink/index.d.ts.map +0 -1
- package/build/types/Markup/Markup.d.ts +0 -18
- package/build/types/Markup/Markup.d.ts.map +0 -1
- package/build/types/Markup/Markup.types.d.ts +0 -68
- 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.injection.test.tsx +0 -152
- package/src/Markup/Markup.less +0 -29
- package/src/Markup/Markup.test.tsx +0 -405
- package/src/Markup/Markup.tsx +0 -165
- package/src/Markup/Markup.types.ts +0 -53
- 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 -227
- 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
- /package/build/types/Calendar/{tableLink → TableLink}/index.d.ts +0 -0
- /package/src/Calendar/{tableLink → TableLink}/index.ts +0 -0
|
@@ -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
|
-
#### <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,227 +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
|
-
* Use the `data` prop whenever the markup template contains values that originate from
|
|
184
|
-
* user-generated content or external API responses (e.g. recipient names, amounts, dates).
|
|
185
|
-
* This separates trusted template structure from untrusted data content, preventing markup
|
|
186
|
-
* injection attacks.
|
|
187
|
-
*
|
|
188
|
-
* All matched Mustache-style placeholders (`{{key}}`) are replaced (including duplicates) with
|
|
189
|
-
* values from the `data` object. If a key is not found, it will be rendered literally as `{{key}}`.
|
|
190
|
-
* Data values are always rendered as plain text — even if they contain markup tags, those are
|
|
191
|
-
* displayed literally and never parsed into elements.
|
|
192
|
-
* */
|
|
193
|
-
export const DataSubstitution: Story = {
|
|
194
|
-
parameters: {
|
|
195
|
-
docs: {
|
|
196
|
-
source: { type: 'dynamic' },
|
|
197
|
-
},
|
|
198
|
-
},
|
|
199
|
-
render: () => (
|
|
200
|
-
<>
|
|
201
|
-
<Markup data={{ amount: '£35.00', recipient: 'Alice' }}>
|
|
202
|
-
{'You sent <strong>{{amount}}</strong> to {{recipient}}. {{amount}} is now pending.'}
|
|
203
|
-
</Markup>
|
|
204
|
-
<Markup data={{ name: '</strong><link href="https://evil.com">Click here</link><strong>' }}>
|
|
205
|
-
{'Hello, <strong>{{name}}</strong>!'}
|
|
206
|
-
</Markup>
|
|
207
|
-
</>
|
|
208
|
-
),
|
|
209
|
-
};
|
|
210
|
-
|
|
211
|
-
/**
|
|
212
|
-
* 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)
|
|
213
|
-
* */
|
|
214
|
-
export const MalformedInput: Story = {
|
|
215
|
-
args: {
|
|
216
|
-
children: [
|
|
217
|
-
'<script>alert("xss")</script>',
|
|
218
|
-
'<img src=x onerror="alert(1)">',
|
|
219
|
-
'<iframe src="https://evil.com"></iframe>',
|
|
220
|
-
'<link href="javascript:alert(1)">blocked protocol</link>',
|
|
221
|
-
'<link href="data:text/html,<script>alert(1)</script>">data URI</link>',
|
|
222
|
-
'<a href="vbscript:MsgBox">vbscript</a>',
|
|
223
|
-
'<link onclick="steal()" href="/safe">ignored attribute</link>',
|
|
224
|
-
'<important>this is safe</important>',
|
|
225
|
-
].join('\n'),
|
|
226
|
-
},
|
|
227
|
-
};
|
|
@@ -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
|
-
);
|