@revenuecat/purchases-ui-js 4.7.2 → 4.7.4
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.
|
@@ -68,6 +68,53 @@
|
|
|
68
68
|
|
|
69
69
|
<Story name="Default" args={{ name: "hello world!" }} />
|
|
70
70
|
|
|
71
|
+
<!--
|
|
72
|
+
Security regression: a localization value containing HTML/JS must render as
|
|
73
|
+
inert text, never execute. With the fix in place this story shows the literal
|
|
74
|
+
markup as text, the page is untouched, and `window.__XSS_FIRED` stays
|
|
75
|
+
undefined. If it ever executes, the page background turns red.
|
|
76
|
+
-->
|
|
77
|
+
<Story
|
|
78
|
+
name="XSS payload is escaped"
|
|
79
|
+
args={{
|
|
80
|
+
font_weight: "regular",
|
|
81
|
+
horizontal_alignment: "leading",
|
|
82
|
+
name: "hello world!",
|
|
83
|
+
text_lid,
|
|
84
|
+
}}
|
|
85
|
+
parameters={{
|
|
86
|
+
localizations: {
|
|
87
|
+
[defaultLocale]: {
|
|
88
|
+
[text_lid]:
|
|
89
|
+
'Premium <img src=x onerror="window.__XSS_FIRED=true;document.body.style.background=\'red\'"> and <svg onload="window.__XSS_FIRED=true"></svg>',
|
|
90
|
+
},
|
|
91
|
+
} satisfies Localizations,
|
|
92
|
+
}}
|
|
93
|
+
/>
|
|
94
|
+
|
|
95
|
+
<!--
|
|
96
|
+
Markdown links: safe http(s)/mailto/tel links are kept (and quoted), while
|
|
97
|
+
dangerous schemes (javascript:, data:, …) have their href dropped so the
|
|
98
|
+
label renders as plain, non-clickable text.
|
|
99
|
+
-->
|
|
100
|
+
<Story
|
|
101
|
+
name="Markdown links (safe kept, dangerous dropped)"
|
|
102
|
+
args={{
|
|
103
|
+
font_weight: "regular",
|
|
104
|
+
horizontal_alignment: "leading",
|
|
105
|
+
name: "hello world!",
|
|
106
|
+
text_lid,
|
|
107
|
+
}}
|
|
108
|
+
parameters={{
|
|
109
|
+
localizations: {
|
|
110
|
+
[defaultLocale]: {
|
|
111
|
+
[text_lid]:
|
|
112
|
+
"Safe: [RevenueCat](https://www.revenuecat.com) — Dangerous: [click me](javascript:window.__XSS_FIRED=true)",
|
|
113
|
+
},
|
|
114
|
+
} satisfies Localizations,
|
|
115
|
+
}}
|
|
116
|
+
/>
|
|
117
|
+
|
|
71
118
|
<Story
|
|
72
119
|
name="Font Weight"
|
|
73
120
|
args={{
|
|
@@ -58,11 +58,49 @@ export function getTextWrapperInlineStyles(colorMode, _restProps, size, backgrou
|
|
|
58
58
|
...mapBackground(colorMode, background_color, null),
|
|
59
59
|
});
|
|
60
60
|
}
|
|
61
|
+
const HTML_ESCAPE_MAP = {
|
|
62
|
+
"&": "&",
|
|
63
|
+
"<": "<",
|
|
64
|
+
">": ">",
|
|
65
|
+
'"': """,
|
|
66
|
+
"'": "'",
|
|
67
|
+
};
|
|
68
|
+
/**
|
|
69
|
+
* Escapes the HTML-special characters in `text` so that user-controlled
|
|
70
|
+
* localization values cannot inject markup. None of the markdown control
|
|
71
|
+
* characters handled below are HTML-special, so escaping first is safe.
|
|
72
|
+
*/
|
|
73
|
+
function escapeHtml(text) {
|
|
74
|
+
return text.replace(/[&<>"']/g, (character) => HTML_ESCAPE_MAP[character]);
|
|
75
|
+
}
|
|
76
|
+
const SAFE_URL_SCHEMES = ["http", "https", "mailto", "tel"];
|
|
77
|
+
/**
|
|
78
|
+
* Returns the URL if it is safe to use as a link `href`, otherwise `null`.
|
|
79
|
+
* Relative and protocol-relative URLs are allowed; absolute URLs are only
|
|
80
|
+
* allowed for an explicit scheme allowlist. Whitespace and control characters
|
|
81
|
+
* (code points <= 0x20) are stripped and the scheme is lowercased before
|
|
82
|
+
* comparison so obfuscated payloads such as "jAvA\tscript:" are still rejected.
|
|
83
|
+
*/
|
|
84
|
+
function sanitizeUrl(url) {
|
|
85
|
+
const normalized = Array.from(url)
|
|
86
|
+
.filter((character) => character.charCodeAt(0) > 0x20)
|
|
87
|
+
.join("")
|
|
88
|
+
.toLowerCase();
|
|
89
|
+
const schemeMatch = normalized.match(/^([a-z][a-z0-9+.-]*):/);
|
|
90
|
+
if (schemeMatch && !SAFE_URL_SCHEMES.includes(schemeMatch[1])) {
|
|
91
|
+
return null;
|
|
92
|
+
}
|
|
93
|
+
return url;
|
|
94
|
+
}
|
|
61
95
|
export function getHtmlFromMarkdown(text) {
|
|
62
96
|
if (!text)
|
|
63
97
|
return "";
|
|
98
|
+
// Escape HTML first so attacker-controlled markup (e.g. `<img onerror=...>`)
|
|
99
|
+
// can never reach the `{@html}` sink. After this, the only HTML in the output
|
|
100
|
+
// is the fixed tag allowlist produced by the rules below.
|
|
101
|
+
const escapedHtml = escapeHtml(text);
|
|
64
102
|
const escapedMarkdownCharacters = [];
|
|
65
|
-
const textWithEscapedMarkdownPlaceholders =
|
|
103
|
+
const textWithEscapedMarkdownPlaceholders = escapedHtml.replaceAll(/\\([\\`*_[\]{}()#+\-.!~])/g, (_, escapedCharacter) => {
|
|
66
104
|
escapedMarkdownCharacters.push(escapedCharacter);
|
|
67
105
|
return `\0RC_ESCAPED_MARKDOWN_${escapedMarkdownCharacters.length - 1}\0`;
|
|
68
106
|
});
|
|
@@ -77,11 +115,20 @@ export function getHtmlFromMarkdown(text) {
|
|
|
77
115
|
},
|
|
78
116
|
link: {
|
|
79
117
|
regexp: /\[(.*?)\]\((.*?)\)/g,
|
|
80
|
-
output:
|
|
118
|
+
output: (_match, label, url) => {
|
|
119
|
+
const safeUrl = sanitizeUrl(url);
|
|
120
|
+
if (safeUrl === null) {
|
|
121
|
+
// Drop the unsafe href and render the (already escaped) label only.
|
|
122
|
+
return label;
|
|
123
|
+
}
|
|
124
|
+
return `<a href="${safeUrl}" target='_blank' rel='noopener noreferrer'>${label}</a>`;
|
|
125
|
+
},
|
|
81
126
|
},
|
|
82
127
|
};
|
|
83
128
|
const parsedText = Object.values(regexpDictionary).reduce((parsedText, { regexp, output }) => {
|
|
84
|
-
return
|
|
129
|
+
return typeof output === "string"
|
|
130
|
+
? parsedText.replaceAll(regexp, output)
|
|
131
|
+
: parsedText.replaceAll(regexp, output);
|
|
85
132
|
}, textWithEscapedMarkdownPlaceholders);
|
|
86
133
|
return escapedMarkdownCharacters.reduce((restoredText, escapedMarkdownCharacter, index) => restoredText.replaceAll(`\0RC_ESCAPED_MARKDOWN_${index}\0`, escapedMarkdownCharacter), parsedText);
|
|
87
134
|
}
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
import type { InitialInputSelections } from "../../stores/inputValidation";
|
|
6
6
|
import type { OnComponentInteraction } from "../../types/paywall-component-interaction";
|
|
7
7
|
import type { WorkflowScreen } from "../../types/workflow";
|
|
8
|
-
import type { VariableDictionary } from "../../types/variables";
|
|
8
|
+
import type { PackageInfo, VariableDictionary } from "../../types/variables";
|
|
9
9
|
import type { WalletButtonRender } from "../../types/wallet";
|
|
10
10
|
import type { UIConfig } from "../../types/ui-config";
|
|
11
11
|
import type { ReservedAttribute } from "../../types/components/input-text";
|
|
@@ -26,6 +26,7 @@
|
|
|
26
26
|
containerId?: string;
|
|
27
27
|
maxContentWidth?: string;
|
|
28
28
|
variablesPerPackage?: Record<string, VariableDictionary>;
|
|
29
|
+
infoPerPackage?: Record<string, PackageInfo>;
|
|
29
30
|
initialInputSelections?: InitialInputSelections;
|
|
30
31
|
onInputChanged?: (
|
|
31
32
|
fieldId: string,
|
|
@@ -59,6 +60,7 @@
|
|
|
59
60
|
containerId = "screen-container",
|
|
60
61
|
maxContentWidth,
|
|
61
62
|
variablesPerPackage,
|
|
63
|
+
infoPerPackage,
|
|
62
64
|
initialInputSelections = {},
|
|
63
65
|
onInputChanged,
|
|
64
66
|
onReservedAttributeChanged,
|
|
@@ -78,6 +80,7 @@
|
|
|
78
80
|
{selectedLocale}
|
|
79
81
|
{uiConfig}
|
|
80
82
|
{variablesPerPackage}
|
|
83
|
+
{infoPerPackage}
|
|
81
84
|
{globalVariables}
|
|
82
85
|
{maxContentWidth}
|
|
83
86
|
{initialInputSelections}
|
|
@@ -3,7 +3,7 @@ import type { ColorScheme } from "../../types/colors";
|
|
|
3
3
|
import type { InitialInputSelections } from "../../stores/inputValidation";
|
|
4
4
|
import type { OnComponentInteraction } from "../../types/paywall-component-interaction";
|
|
5
5
|
import type { WorkflowScreen } from "../../types/workflow";
|
|
6
|
-
import type { VariableDictionary } from "../../types/variables";
|
|
6
|
+
import type { PackageInfo, VariableDictionary } from "../../types/variables";
|
|
7
7
|
import type { WalletButtonRender } from "../../types/wallet";
|
|
8
8
|
import type { UIConfig } from "../../types/ui-config";
|
|
9
9
|
import type { ReservedAttribute } from "../../types/components/input-text";
|
|
@@ -21,6 +21,7 @@ interface Props {
|
|
|
21
21
|
containerId?: string;
|
|
22
22
|
maxContentWidth?: string;
|
|
23
23
|
variablesPerPackage?: Record<string, VariableDictionary>;
|
|
24
|
+
infoPerPackage?: Record<string, PackageInfo>;
|
|
24
25
|
initialInputSelections?: InitialInputSelections;
|
|
25
26
|
onInputChanged?: (fieldId: string, value: string, actionId?: string) => void;
|
|
26
27
|
onReservedAttributeChanged?: (reservedAttribute: ReservedAttribute, value: string) => void;
|