@transferwise/components 0.0.0-experimental-3586f3f → 0.0.0-experimental-a06eb0d

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.
@@ -1 +1 @@
1
- {"version":3,"file":"sanitise.mjs","sources":["../../../src/Markup/utils/sanitise.ts"],"sourcesContent":["import { ALLOWED_PROTOCOLS } from '../constants';\n\nconst INVISIBLE_CHARS = /[\\u200b\\u200c\\u200d\\ufeff\\u00ad\\u2060\\u180e]/g;\n// eslint-disable-next-line regexp/no-control-character\nconst CONTROL_CHARS = /[\\u0000-\\u001f\\u007f-\\u009f]/;\nconst DANGEROUS_HTML_CHARS = /[<>\"']/;\nconst DANGEROUS_SCHEME = /^(?:javascript|vbscript|data|jscript)\\s*:/i;\n// eslint-disable-next-line regexp/no-useless-non-capturing-group\nconst ENCODED_COLON_SCHEME = /^(?:javascript|vbscript|data|jscript)(?:%3a)/i;\nconst IPV6_BRACKETS = /^\\[|\\]$/g;\nconst NUMERIC_IP = /^[\\d.]+$/;\nconst HEX_IP = /^0x[0-9a-f]+$/i;\n\nexport function isAllowedHref(href: string): boolean {\n const cleaned = href.replace(INVISIBLE_CHARS, '');\n const trimmed = cleaned.trim();\n\n if (!trimmed) {\n return false;\n }\n\n if (CONTROL_CHARS.test(trimmed)) {\n return false;\n }\n\n if (DANGEROUS_HTML_CHARS.test(trimmed)) {\n return false;\n }\n\n if (DANGEROUS_SCHEME.test(trimmed)) {\n return false;\n }\n\n if (ENCODED_COLON_SCHEME.test(trimmed)) {\n return false;\n }\n\n if (trimmed.startsWith('//')) {\n return false;\n }\n\n if (trimmed.startsWith('\\\\')) {\n return false;\n }\n\n if (trimmed.startsWith('/')) {\n return trimmed[1] !== '\\\\';\n }\n\n if (trimmed.startsWith('#')) {\n return true;\n }\n\n if (trimmed.startsWith('./') || trimmed.startsWith('../')) {\n return false;\n }\n\n try {\n const url = new URL(trimmed);\n\n if (!ALLOWED_PROTOCOLS.includes(url.protocol)) {\n return false;\n }\n\n if (url.protocol === 'https:') {\n if (!trimmed.startsWith('https://')) {\n return false;\n }\n\n if (!url.hostname) {\n return false;\n }\n\n if (url.username || url.password) {\n return false;\n }\n\n if (isInternalHost(url.hostname)) {\n return false;\n }\n }\n\n if (url.protocol === 'mailto:') {\n if (url.search) {\n return false;\n }\n\n if (!url.pathname) {\n return false;\n }\n }\n\n return true;\n } catch {\n return false;\n }\n}\n\nexport type AllowLinksMode = 'internal' | 'all' | 'none';\n\n// Synthetic origin used to resolve relative paths via the WHATWG URL parser.\n// If the resolved origin still equals this sentinel, the path stays on-origin.\nconst SSR_SENTINEL_ORIGIN = 'https://internal.invalid';\n\nexport function isHrefAllowedByMode(href: string, mode: AllowLinksMode): boolean {\n switch (mode) {\n case 'none':\n return false;\n case 'all':\n return true;\n case 'internal':\n try {\n if (!href.startsWith('/')) {\n const parsed = new URL(href);\n return parsed.protocol === 'https:' && parsed.hostname === 'wise.com';\n }\n const parsed = new URL(href, SSR_SENTINEL_ORIGIN);\n return parsed.origin === SSR_SENTINEL_ORIGIN;\n } catch {\n return false;\n }\n }\n}\n\nfunction isInternalHost(hostname: string): boolean {\n const stripped = hostname.replace(IPV6_BRACKETS, '');\n const host = stripped.endsWith('.') ? stripped.slice(0, -1) : stripped;\n if (NUMERIC_IP.test(host)) return true;\n if (HEX_IP.test(host)) return true;\n if (host === '::1' || host === '0:0:0:0:0:0:0:1') return true;\n if (host === 'localhost') return true;\n if (host.endsWith('.internal')) return true;\n return false;\n}\n"],"names":["INVISIBLE_CHARS","CONTROL_CHARS","DANGEROUS_HTML_CHARS","DANGEROUS_SCHEME","ENCODED_COLON_SCHEME","IPV6_BRACKETS","NUMERIC_IP","HEX_IP","isAllowedHref","href","cleaned","replace","trimmed","trim","test","startsWith","url","URL","ALLOWED_PROTOCOLS","includes","protocol","hostname","username","password","isInternalHost","search","pathname","SSR_SENTINEL_ORIGIN","isHrefAllowedByMode","mode","parsed","origin","stripped","host","endsWith","slice"],"mappings":"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"}
1
+ {"version":3,"file":"sanitise.mjs","sources":["../../../src/Markup/utils/sanitise.ts"],"sourcesContent":["import { ALLOWED_PROTOCOLS } from '../constants';\n\nconst INVISIBLE_CHARS = /[\\u200b\\u200c\\u200d\\ufeff\\u00ad\\u2060\\u180e]/g;\n// eslint-disable-next-line regexp/no-control-character\nconst CONTROL_CHARS = /[\\u0000-\\u001f\\u007f-\\u009f]/;\nconst DANGEROUS_HTML_CHARS = /[<>\"']/;\nconst DANGEROUS_SCHEME = /^(?:javascript|vbscript|data|jscript)\\s*:/i;\n// eslint-disable-next-line regexp/no-useless-non-capturing-group\nconst ENCODED_COLON_SCHEME = /^(?:javascript|vbscript|data|jscript)(?:%3a)/i;\nconst IPV6_BRACKETS = /^\\[|\\]$/g;\nconst NUMERIC_IP = /^[\\d.]+$/;\nconst HEX_IP = /^0x[0-9a-f]+$/i;\n\nexport function sanitiseHref(href: string): string | undefined {\n const cleaned = href.replace(INVISIBLE_CHARS, '');\n const trimmed = cleaned.trim();\n\n if (!trimmed) {\n return undefined;\n }\n\n if (CONTROL_CHARS.test(trimmed)) {\n return undefined;\n }\n\n if (DANGEROUS_HTML_CHARS.test(trimmed)) {\n return undefined;\n }\n\n if (DANGEROUS_SCHEME.test(trimmed)) {\n return undefined;\n }\n\n if (ENCODED_COLON_SCHEME.test(trimmed)) {\n return undefined;\n }\n\n if (trimmed.startsWith('//')) {\n return undefined;\n }\n\n if (trimmed.startsWith('\\\\')) {\n return undefined;\n }\n\n if (trimmed.startsWith('/')) {\n return trimmed[1] !== '\\\\' ? trimmed : undefined;\n }\n\n if (trimmed.startsWith('#')) {\n return trimmed;\n }\n\n if (trimmed.startsWith('./') || trimmed.startsWith('../')) {\n return undefined;\n }\n\n try {\n const url = new URL(trimmed);\n\n if (!ALLOWED_PROTOCOLS.includes(url.protocol)) {\n return undefined;\n }\n\n if (url.protocol === 'https:') {\n if (!trimmed.startsWith('https://')) {\n return undefined;\n }\n\n if (!url.hostname) {\n return undefined;\n }\n\n if (url.username || url.password) {\n return undefined;\n }\n\n if (isInternalHost(url.hostname)) {\n return undefined;\n }\n }\n\n if (url.protocol === 'mailto:') {\n if (url.search) {\n return undefined;\n }\n\n if (!url.pathname) {\n return undefined;\n }\n }\n\n return trimmed;\n } catch {\n return undefined;\n }\n}\n\nexport type AllowLinksMode = 'internal' | 'all' | 'none';\n\n// Synthetic origin used to resolve relative paths via the WHATWG URL parser.\n// If the resolved origin still equals this sentinel, the path stays on-origin.\nconst SSR_SENTINEL_ORIGIN = 'https://internal.invalid';\n\nexport function isHrefAllowedByMode(href: string, mode: AllowLinksMode): boolean {\n switch (mode) {\n case 'none':\n return false;\n case 'all':\n return true;\n case 'internal':\n try {\n if (!href.startsWith('/')) {\n const parsed = new URL(href);\n return parsed.protocol === 'https:' && parsed.hostname === 'wise.com';\n }\n const parsed = new URL(href, SSR_SENTINEL_ORIGIN);\n return parsed.origin === SSR_SENTINEL_ORIGIN;\n } catch {\n return false;\n }\n }\n}\n\nfunction isInternalHost(hostname: string): boolean {\n const stripped = hostname.replace(IPV6_BRACKETS, '');\n const host = stripped.endsWith('.') ? stripped.slice(0, -1) : stripped;\n if (NUMERIC_IP.test(host)) return true;\n if (HEX_IP.test(host)) return true;\n if (host === '::1' || host === '0:0:0:0:0:0:0:1') return true;\n if (host === 'localhost') return true;\n if (host.endsWith('.internal')) return true;\n return false;\n}\n"],"names":["INVISIBLE_CHARS","CONTROL_CHARS","DANGEROUS_HTML_CHARS","DANGEROUS_SCHEME","ENCODED_COLON_SCHEME","IPV6_BRACKETS","NUMERIC_IP","HEX_IP","sanitiseHref","href","cleaned","replace","trimmed","trim","undefined","test","startsWith","url","URL","ALLOWED_PROTOCOLS","includes","protocol","hostname","username","password","isInternalHost","search","pathname","SSR_SENTINEL_ORIGIN","isHrefAllowedByMode","mode","parsed","origin","stripped","host","endsWith","slice"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"Markup.d.ts","sourceRoot":"","sources":["../../../src/Markup/Markup.tsx"],"names":[],"mappings":"AAAA,OAAO,KAA4B,MAAM,OAAO,CAAC;AAMjD,OAAO,KAAK,EAA6B,WAAW,EAAE,MAAM,gBAAgB,CAAC;AA+G7E;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,MAAM,EAAE,KAAK,CAAC,EAAE,CAAC,WAAW,CAoCxC,CAAC;AAEF,eAAe,MAAM,CAAC"}
1
+ {"version":3,"file":"Markup.d.ts","sourceRoot":"","sources":["../../../src/Markup/Markup.tsx"],"names":[],"mappings":"AAAA,OAAO,KAA4B,MAAM,OAAO,CAAC;AAMjD,OAAO,KAAK,EAA6B,WAAW,EAAE,MAAM,gBAAgB,CAAC;AAqH7E;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,MAAM,EAAE,KAAK,CAAC,EAAE,CAAC,WAAW,CAoCxC,CAAC;AAEF,eAAe,MAAM,CAAC"}
@@ -1 +1 @@
1
- {"version":3,"file":"parseMarkup.d.ts","sourceRoot":"","sources":["../../../../src/Markup/utils/parseMarkup.ts"],"names":[],"mappings":"AAAA,OAAO,EAAiC,iBAAiB,EAAe,MAAM,cAAc,CAAC;AAC7F,OAAO,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAE7C,OAAO,EAAE,iBAAiB,EAAE,CAAC;AAC7B,eAAO,MAAM,gBAAgB,QAAS,CAAC;AACvC,eAAO,MAAM,8BAA8B,MAAM,CAAC;AAyOlD,oGAAoG;AACpG,wBAAgB,WAAW,CAAC,KAAK,EAAE,MAAM,GAAG,UAAU,EAAE,CAYvD"}
1
+ {"version":3,"file":"parseMarkup.d.ts","sourceRoot":"","sources":["../../../../src/Markup/utils/parseMarkup.ts"],"names":[],"mappings":"AAAA,OAAO,EAAiC,iBAAiB,EAAe,MAAM,cAAc,CAAC;AAC7F,OAAO,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAE7C,OAAO,EAAE,iBAAiB,EAAE,CAAC;AAC7B,eAAO,MAAM,gBAAgB,QAAS,CAAC;AACvC,eAAO,MAAM,8BAA8B,MAAM,CAAC;AAsPlD,oGAAoG;AACpG,wBAAgB,WAAW,CAAC,KAAK,EAAE,MAAM,GAAG,UAAU,EAAE,CAYvD"}
@@ -1,4 +1,4 @@
1
- export declare function isAllowedHref(href: string): boolean;
1
+ export declare function sanitiseHref(href: string): string | undefined;
2
2
  export type AllowLinksMode = 'internal' | 'all' | 'none';
3
3
  export declare function isHrefAllowedByMode(href: string, mode: AllowLinksMode): boolean;
4
4
  //# sourceMappingURL=sanitise.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"sanitise.d.ts","sourceRoot":"","sources":["../../../../src/Markup/utils/sanitise.ts"],"names":[],"mappings":"AAaA,wBAAgB,aAAa,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAmFnD;AAED,MAAM,MAAM,cAAc,GAAG,UAAU,GAAG,KAAK,GAAG,MAAM,CAAC;AAMzD,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,cAAc,GAAG,OAAO,CAkB/E"}
1
+ {"version":3,"file":"sanitise.d.ts","sourceRoot":"","sources":["../../../../src/Markup/utils/sanitise.ts"],"names":[],"mappings":"AAaA,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAmF7D;AAED,MAAM,MAAM,cAAc,GAAG,UAAU,GAAG,KAAK,GAAG,MAAM,CAAC;AAMzD,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,cAAc,GAAG,OAAO,CAkB/E"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@transferwise/components",
3
- "version": "0.0.0-experimental-3586f3f",
3
+ "version": "0.0.0-experimental-a06eb0d",
4
4
  "description": "Neptune React components",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {
@@ -3,7 +3,7 @@ import React, { Fragment, useMemo } from 'react';
3
3
  import Link from '../Link';
4
4
 
5
5
  import { parseMarkup } from './utils/parseMarkup';
6
- import { type AllowLinksMode, isAllowedHref, isHrefAllowedByMode } from './utils/sanitise';
6
+ import { type AllowLinksMode, isHrefAllowedByMode, sanitiseHref } from './utils/sanitise';
7
7
  import type { MarkupActions, MarkupNode, MarkupProps } from './Markup.types';
8
8
 
9
9
  const PLACEHOLDER_REGEX = /\{\{(\w+)\}\}/g;
@@ -43,9 +43,15 @@ function renderLinkNode(
43
43
  );
44
44
  }
45
45
 
46
- if (node.href && isAllowedHref(node.href) && isHrefAllowedByMode(node.href, allowLinks)) {
46
+ const sanitisedHref = node.href ? sanitiseHref(node.href) : undefined;
47
+ if (sanitisedHref && isHrefAllowedByMode(sanitisedHref, allowLinks)) {
47
48
  return (
48
- <Link key={key} href={node.href} target={node.target} aria-label={node.accessibilityLabel}>
49
+ <Link
50
+ key={key}
51
+ href={sanitisedHref}
52
+ target={node.target}
53
+ aria-label={node.accessibilityLabel}
54
+ >
49
55
  {children}
50
56
  </Link>
51
57
  );
@@ -495,7 +495,7 @@ describe('parseMarkup', () => {
495
495
  it('captures full href even when it contains > inside quotes', () => {
496
496
  const result = parseMarkup('<link href="</link>">remaining text</link>');
497
497
  // TAG_REGEX sees through > inside quotes — full href is captured
498
- // isAllowedHref (at render time) blocks it because it contains <
498
+ // sanitiseHref (at render time) blocks it because it contains <
499
499
  expect(result).toEqual([
500
500
  {
501
501
  type: 'link',
@@ -636,10 +636,10 @@ describe('parseMarkup', () => {
636
636
  });
637
637
 
638
638
  describe('SVG/IMG in attribute values', () => {
639
- it('extracts href containing img tag (blocked by isAllowedHref at render)', () => {
639
+ it('extracts href containing img tag (blocked by sanitiseHref at render)', () => {
640
640
  const result = parseMarkup('<link href="<img src=x onerror=alert(1)>">text</link>');
641
641
  // TAG_REGEX now captures > inside quotes — full href is extracted
642
- // isAllowedHref blocks it at render time (contains < and >)
642
+ // sanitiseHref blocks it at render time (contains < and >)
643
643
  expect(result).toEqual([
644
644
  {
645
645
  type: 'link',
@@ -668,13 +668,13 @@ describe('parseMarkup', () => {
668
668
  });
669
669
 
670
670
  it('parses normally at exactly MAX_INPUT_LENGTH', () => {
671
- const input = `<important>${ 'x'.repeat(MAX_INPUT_LENGTH - 24) }</important>`;
671
+ const input = `<important>${'x'.repeat(MAX_INPUT_LENGTH - 24)}</important>`;
672
672
  const result = parseMarkup(input);
673
673
  expect(result[0]).toHaveProperty('type', 'important');
674
674
  });
675
675
 
676
676
  it('does not parse markup tags in oversized input', () => {
677
- const input = `<important>text</important>${ 'x'.repeat(MAX_INPUT_LENGTH)}`;
677
+ const input = `<important>text</important>${'x'.repeat(MAX_INPUT_LENGTH)}`;
678
678
  const result = parseMarkup(input);
679
679
  expect(result).toEqual([{ type: 'text', content: input }]);
680
680
  });
@@ -175,6 +175,19 @@ function buildNode(rawTag: string, attrs: string, children: MarkupNode[]): Marku
175
175
  } as MarkupNode;
176
176
  }
177
177
 
178
+ /**
179
+ * Matches allow-listed attributes from the raw attribute string captured by TAG_REGEX.
180
+ * Requires whitespace before each attribute name to prevent matching inside quoted values.
181
+ *
182
+ * Example: for input ` href="https://wise.com" target="_blank"`
183
+ * - Match 1: groups = ['href', 'https://wise.com', undefined]
184
+ * - Match 2: groups = ['target', '_blank', undefined]
185
+ *
186
+ * Capture groups:
187
+ * 1. Attribute name (href, action, target, or accessibilityLabel)
188
+ * 2. Double-quoted value (if present)
189
+ * 3. Single-quoted value (if present)
190
+ */
178
191
  const ATTR_REGEX = /(?<=\s)(href|action|target|accessibilityLabel)=(?:"([^"]*)"|'([^']*)')/g;
179
192
 
180
193
  type ParsedAttributes = {