@transferwise/components 46.155.1 → 46.156.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/build/Markup/Markup.js +138 -0
- package/build/Markup/Markup.js.map +1 -0
- package/build/Markup/Markup.mjs +133 -0
- package/build/Markup/Markup.mjs.map +1 -0
- package/build/Markup/constants.js +21 -0
- package/build/Markup/constants.js.map +1 -0
- package/build/Markup/constants.mjs +15 -0
- package/build/Markup/constants.mjs.map +1 -0
- package/build/Markup/utils/parseMarkup.js +263 -0
- package/build/Markup/utils/parseMarkup.js.map +1 -0
- package/build/Markup/utils/parseMarkup.mjs +258 -0
- package/build/Markup/utils/parseMarkup.mjs.map +1 -0
- package/build/Markup/utils/sanitise.js +111 -0
- package/build/Markup/utils/sanitise.js.map +1 -0
- package/build/Markup/utils/sanitise.mjs +108 -0
- package/build/Markup/utils/sanitise.mjs.map +1 -0
- package/build/i18n/hu.json +1 -1
- package/build/i18n/hu.json.js +1 -1
- package/build/i18n/hu.json.mjs +1 -1
- package/build/index.js +2 -0
- package/build/index.js.map +1 -1
- package/build/index.mjs +1 -0
- package/build/index.mjs.map +1 -1
- package/build/main.css +35 -0
- package/build/styles/Markup/Markup.css +28 -0
- package/build/styles/Markup/_stories/Vulnerability/Vulnerability.css +69 -0
- package/build/styles/main.css +35 -0
- package/build/types/Body/Body.d.ts +2 -2
- package/build/types/IconButton/IconButton.d.ts +1 -1
- package/build/types/Markup/Markup.d.ts +18 -0
- package/build/types/Markup/Markup.d.ts.map +1 -0
- package/build/types/Markup/Markup.types.d.ts +77 -0
- package/build/types/Markup/Markup.types.d.ts.map +1 -0
- package/build/types/Markup/_stories/Vulnerability/Vulnerability.d.ts +12 -0
- package/build/types/Markup/_stories/Vulnerability/Vulnerability.d.ts.map +1 -0
- package/build/types/Markup/constants.d.ts +6 -0
- package/build/types/Markup/constants.d.ts.map +1 -0
- package/build/types/Markup/index.d.ts +3 -0
- package/build/types/Markup/index.d.ts.map +1 -0
- package/build/types/Markup/utils/parseMarkup.d.ts +8 -0
- package/build/types/Markup/utils/parseMarkup.d.ts.map +1 -0
- package/build/types/Markup/utils/sanitise.d.ts +4 -0
- package/build/types/Markup/utils/sanitise.d.ts.map +1 -0
- package/build/types/MoneyInput/MoneyInput.d.ts +1 -1
- package/build/types/MoneyInput/MoneyInput.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/build/types/index.d.ts +2 -0
- package/build/types/index.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/Markup/Markup.css +28 -0
- package/src/Markup/Markup.injection.test.tsx +163 -0
- package/src/Markup/Markup.less +29 -0
- package/src/Markup/Markup.test.tsx +502 -0
- package/src/Markup/Markup.tsx +175 -0
- package/src/Markup/Markup.types.ts +62 -0
- package/src/Markup/_stories/Markup.docs.mdx +75 -0
- package/src/Markup/_stories/Markup.security.docs.mdx +343 -0
- package/src/Markup/_stories/Markup.story.tsx +266 -0
- package/src/Markup/_stories/Vulnerability/Vulnerability.css +69 -0
- package/src/Markup/_stories/Vulnerability/Vulnerability.tsx +30 -0
- package/src/Markup/constants.ts +23 -0
- package/src/Markup/index.ts +2 -0
- package/src/Markup/utils/parseMarkup.test.ts +706 -0
- package/src/Markup/utils/parseMarkup.ts +265 -0
- package/src/Markup/utils/sanitise.test.ts +499 -0
- package/src/Markup/utils/sanitise.ts +129 -0
- package/src/i18n/hu.json +1 -1
- package/src/index.ts +2 -0
- package/src/main.css +35 -0
- package/src/main.less +1 -0
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
|
|
3
|
+
import { render } from '../test-utils';
|
|
4
|
+
|
|
5
|
+
import { Markup } from './Markup';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Proof of Concept: Markup Injection via Attacker-Controlled Data
|
|
9
|
+
*
|
|
10
|
+
* These tests demonstrate Laszlo's concern: when untrusted data (e.g. a payee name,
|
|
11
|
+
* company name, or any string from an external source) is interpolated into a markup
|
|
12
|
+
* template string, the attacker can break out of their structural context and inject
|
|
13
|
+
* arbitrary allowed markup — links, emphasis, etc.
|
|
14
|
+
*
|
|
15
|
+
* This is NOT about XSS (script execution is blocked). It's about content spoofing
|
|
16
|
+
* and phishing within trusted UI.
|
|
17
|
+
*/
|
|
18
|
+
describe('Markup injection via attacker-controlled data', () => {
|
|
19
|
+
describe('the vulnerability: structural injection', () => {
|
|
20
|
+
it('default allowedHrefs blocks injected external links', () => {
|
|
21
|
+
const attackerControlledName =
|
|
22
|
+
'Acme Corp</strong>' +
|
|
23
|
+
'<link href="https://evil.example.com">' +
|
|
24
|
+
'<important>ACTION REQUIRED: Verify your identity</important>' +
|
|
25
|
+
'</link>' +
|
|
26
|
+
'<strong>';
|
|
27
|
+
|
|
28
|
+
const template = `Transfer <strong>£35.00</strong> to <strong>${attackerControlledName}</strong>`;
|
|
29
|
+
|
|
30
|
+
const { container } = render(<Markup>{template}</Markup>);
|
|
31
|
+
|
|
32
|
+
// The default allowlist blocks external domains — the link is NOT rendered
|
|
33
|
+
const injectedLink = container.querySelector('a[href="https://evil.example.com"]');
|
|
34
|
+
expect(injectedLink).not.toBeInTheDocument();
|
|
35
|
+
|
|
36
|
+
// But the structural injection still works (emphasis renders)
|
|
37
|
+
const injectedEmphasis = container.querySelector('.wds-markup-emphasis--important');
|
|
38
|
+
expect(injectedEmphasis).toBeInTheDocument();
|
|
39
|
+
expect(injectedEmphasis).toHaveTextContent('ACTION REQUIRED: Verify your identity');
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it('injected external links render when allowLinks="all"', () => {
|
|
43
|
+
const attackerControlledName =
|
|
44
|
+
'Acme Corp</strong>' +
|
|
45
|
+
'<link href="https://evil.example.com">' +
|
|
46
|
+
'<important>ACTION REQUIRED: Verify your identity</important>' +
|
|
47
|
+
'</link>' +
|
|
48
|
+
'<strong>';
|
|
49
|
+
|
|
50
|
+
const template = `Transfer <strong>£35.00</strong> to <strong>${attackerControlledName}</strong>`;
|
|
51
|
+
|
|
52
|
+
const { container } = render(<Markup allowLinks="all">{template}</Markup>);
|
|
53
|
+
|
|
54
|
+
// With allowLinks="all", the link renders — demonstrating the structural injection
|
|
55
|
+
const injectedLink = container.querySelector('a[href="https://evil.example.com"]');
|
|
56
|
+
expect(injectedLink).toBeInTheDocument();
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it('default allowedHrefs blocks phishing links in paragraph injection', () => {
|
|
60
|
+
const attackerNote =
|
|
61
|
+
'Thanks!</p>' +
|
|
62
|
+
'<p><important>Your account has been compromised. ' +
|
|
63
|
+
'Contact support at </important>' +
|
|
64
|
+
'<link href="https://phishing.example.com">this secure link</link></p>' +
|
|
65
|
+
'<p>';
|
|
66
|
+
|
|
67
|
+
const template = `<p>Note from sender: ${attackerNote}</p>`;
|
|
68
|
+
|
|
69
|
+
const { container } = render(<Markup>{template}</Markup>);
|
|
70
|
+
|
|
71
|
+
const phishingLink = container.querySelector('a[href="https://phishing.example.com"]');
|
|
72
|
+
expect(phishingLink).not.toBeInTheDocument();
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it('attacker can inject misleading accessibility labels', () => {
|
|
76
|
+
const attackerValue =
|
|
77
|
+
'normal text</strong>' +
|
|
78
|
+
'<strong accessibilityLabel="Security alert: click to verify">' +
|
|
79
|
+
'<link href="https://evil.example.com">here</link>' +
|
|
80
|
+
'</strong>' +
|
|
81
|
+
'<strong>';
|
|
82
|
+
|
|
83
|
+
const template = `Your balance: <strong>${attackerValue}</strong>`;
|
|
84
|
+
|
|
85
|
+
const { container } = render(<Markup>{template}</Markup>);
|
|
86
|
+
|
|
87
|
+
const misleadingElement = container.querySelector(
|
|
88
|
+
'[aria-label="Security alert: click to verify"]',
|
|
89
|
+
);
|
|
90
|
+
expect(misleadingElement).toBeInTheDocument();
|
|
91
|
+
});
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
describe('what IS blocked (XSS / script execution)', () => {
|
|
95
|
+
it('cannot inject arbitrary HTML elements', () => {
|
|
96
|
+
const xssAttempt = '<img src=x onerror=alert(1)>';
|
|
97
|
+
const template = `Hello <strong>${xssAttempt}</strong>`;
|
|
98
|
+
|
|
99
|
+
const { container } = render(<Markup>{template}</Markup>);
|
|
100
|
+
|
|
101
|
+
// <img> is not in the allow-list, so it's rendered as text
|
|
102
|
+
expect(container.querySelector('img')).not.toBeInTheDocument();
|
|
103
|
+
expect(container).toHaveTextContent(/<img src=x onerror=alert\(1\)>/);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it('cannot inject script tags', () => {
|
|
107
|
+
const xssAttempt = '<script>alert("xss")</script>';
|
|
108
|
+
const template = `Hello <strong>${xssAttempt}</strong>`;
|
|
109
|
+
|
|
110
|
+
const { container } = render(<Markup>{template}</Markup>);
|
|
111
|
+
|
|
112
|
+
expect(container.querySelector('script')).not.toBeInTheDocument();
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it('blocks javascript: URLs in links', () => {
|
|
116
|
+
// Even if attacker injects a link, javascript: URLs are blocked
|
|
117
|
+
const attack = 'click</strong><link href="javascript:alert(1)">me</link><strong>';
|
|
118
|
+
const template = `<strong>${attack}</strong>`;
|
|
119
|
+
|
|
120
|
+
const { container } = render(<Markup>{template}</Markup>);
|
|
121
|
+
|
|
122
|
+
// The link renders but without the dangerous href (falls back to Fragment)
|
|
123
|
+
expect(container.querySelector('a[href*="javascript"]')).not.toBeInTheDocument();
|
|
124
|
+
});
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
describe('the general nature of this problem', () => {
|
|
128
|
+
it('is identical to HTML injection via innerHTML', () => {
|
|
129
|
+
// This is the same class of bug — mixing code and data in a string
|
|
130
|
+
const attackerName = '<b>URGENT</b>';
|
|
131
|
+
const div = document.createElement('div');
|
|
132
|
+
div.innerHTML = `Hello ${attackerName}`;
|
|
133
|
+
|
|
134
|
+
// The attacker's data became structure
|
|
135
|
+
expect(div.querySelector('b')).not.toBeNull();
|
|
136
|
+
expect(div.querySelector('b')!).toHaveTextContent('URGENT');
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
it('react JSX prevents this by design', () => {
|
|
140
|
+
// In contrast, React treats variables as pure data — never as structure
|
|
141
|
+
const attackerName = '<strong>URGENT</strong>';
|
|
142
|
+
|
|
143
|
+
const { container } = render(<span>Hello {attackerName}</span>);
|
|
144
|
+
|
|
145
|
+
// React escapes the string — it renders as visible text, not as a <strong> tag
|
|
146
|
+
expect(container.querySelector('strong')).not.toBeInTheDocument();
|
|
147
|
+
expect(container).toHaveTextContent(/<strong>URGENT<\/strong>/);
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
it('demonstrates why Markup reintroduces the problem React solved', () => {
|
|
151
|
+
// The same attacker string that React handles safely...
|
|
152
|
+
const attackerName = '<strong>URGENT: Call +44 123 456 789 NOW</strong>';
|
|
153
|
+
|
|
154
|
+
// ...becomes dangerous when passed through Markup's string parser
|
|
155
|
+
const { container } = render(<Markup>{`Hello ${attackerName}`}</Markup>);
|
|
156
|
+
|
|
157
|
+
// The attacker's data has been promoted to structure
|
|
158
|
+
const injected = container.querySelector('strong');
|
|
159
|
+
expect(injected).toBeInTheDocument();
|
|
160
|
+
expect(injected).toHaveTextContent('URGENT: Call +44 123 456 789 NOW');
|
|
161
|
+
});
|
|
162
|
+
});
|
|
163
|
+
});
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
.wds-markup-emphasis {
|
|
2
|
+
font-weight: var(--font-weight-semi-bold);
|
|
3
|
+
font-style: normal;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
.wds-markup-emphasis--important {
|
|
7
|
+
color: var(--color-content-primary);
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
.wds-markup-emphasis--positive {
|
|
11
|
+
color: var(--color-content-positive);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
.wds-markup-emphasis--negative {
|
|
15
|
+
color: var(--color-content-negative);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
.wds-markup-strong {
|
|
19
|
+
font-weight: var(--font-weight-semi-bold);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/// @see https://www.webaxe.org/strikethrough-html-accessibility/
|
|
23
|
+
.wds-markup-strikethrough {
|
|
24
|
+
text-decoration: line-through;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
.wds-markup-paragraph {
|
|
28
|
+
margin: 0;
|
|
29
|
+
}
|