@shopgate/pwa-common 7.31.8-beta.1 → 7.31.9-beta.1
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.
|
@@ -6,6 +6,7 @@ import "core-js/modules/web.url-search-params.js";
|
|
|
6
6
|
import { logger } from '@shopgate/pwa-core/helpers';
|
|
7
7
|
import { DEEPLINK_CART_ADD_PRODUCT_PATTERN } from '@shopgate/pwa-common-commerce/cart/constants';
|
|
8
8
|
import fetchProduct from '@shopgate/pwa-common-commerce/product/actions/fetchProduct';
|
|
9
|
+
import { sanitizeLink } from "../../helpers/router";
|
|
9
10
|
import { historyPush, historyReset } from "../router";
|
|
10
11
|
import { INDEX_PATH_DEEPLINK, INDEX_PATH } from "../../constants/RoutePaths";
|
|
11
12
|
|
|
@@ -24,8 +25,11 @@ export default function handleLink(payload, allowExternalLinks = false) {
|
|
|
24
25
|
return;
|
|
25
26
|
}
|
|
26
27
|
|
|
27
|
-
//
|
|
28
|
-
link = String(link);
|
|
28
|
+
// Cast to string and remove potentially malicious content from the external link
|
|
29
|
+
link = sanitizeLink(String(link));
|
|
30
|
+
if (!link) {
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
29
33
|
let pathname;
|
|
30
34
|
if (link.startsWith('http')) {
|
|
31
35
|
// Link is common URL schema.
|
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Unescape HTML entities.
|
|
3
|
+
* The input is assigned to a textarea element whose content is parsed as plain text, so markup
|
|
4
|
+
* within the input is never turned into DOM nodes and can't execute scripts.
|
|
3
5
|
* @param {string} input The escaped HTML.
|
|
4
6
|
* @returns {string} The unescaped HTML.
|
|
5
7
|
*/
|
|
6
8
|
const decodeHTML = input => {
|
|
7
|
-
const
|
|
8
|
-
|
|
9
|
-
return
|
|
9
|
+
const textarea = document.createElement('textarea');
|
|
10
|
+
textarea.innerHTML = input;
|
|
11
|
+
return textarea.value;
|
|
10
12
|
};
|
|
11
13
|
export default decodeHTML;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Converts a string which might contain HTML markup or HTML entities into plain text.
|
|
3
|
+
* Tags are stripped and entities are decoded. The input is parsed within an inert document
|
|
4
|
+
* created by DOMParser, so scripts are not executed and no resources are loaded.
|
|
5
|
+
* @param {string} input The input string.
|
|
6
|
+
* @returns {string} The plain text.
|
|
7
|
+
*/
|
|
8
|
+
const htmlToText = input => {
|
|
9
|
+
if (typeof input !== 'string' || input === '') {
|
|
10
|
+
return '';
|
|
11
|
+
}
|
|
12
|
+
const doc = new DOMParser().parseFromString(input, 'text/html');
|
|
13
|
+
return doc.body?.textContent ?? '';
|
|
14
|
+
};
|
|
15
|
+
export default htmlToText;
|
package/helpers/router/index.js
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import "core-js/modules/es.array.reduce.js";
|
|
2
|
+
import "core-js/modules/es.string.replace.js";
|
|
2
3
|
import "core-js/modules/web.url-search-params.js";
|
|
3
4
|
import { createBrowserHistory } from 'history';
|
|
4
5
|
import { router } from '@virtuous/conductor';
|
|
6
|
+
import { logger } from '@shopgate/pwa-core/helpers';
|
|
5
7
|
const match = /^(.*)index.html/.exec(window.location.pathname);
|
|
6
8
|
const {
|
|
7
9
|
getCurrentRoute
|
|
@@ -56,4 +58,87 @@ export const parseObjectToQueryString = (obj, includePrefix = true) => {
|
|
|
56
58
|
return `?${urlParams.toString()}`;
|
|
57
59
|
}
|
|
58
60
|
return urlParams.toString();
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
// Angle brackets and their HTML entity representations.
|
|
64
|
+
const ANGLE_BRACKET_ENTITIES = '&(lt|gt|#0*6[02]|#x0*3[ce]);?';
|
|
65
|
+
const UNSAFE_CHARS_REGEX = new RegExp(`[<>]|${ANGLE_BRACKET_ENTITIES}`, 'i');
|
|
66
|
+
const UNSAFE_PROTOCOL_REGEX = /^\s*(javascript|data|vbscript):/i;
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Removes HTML tags, angle brackets and their entity representations from a string.
|
|
70
|
+
* @param {string} value The (decoded) value.
|
|
71
|
+
* @returns {string}
|
|
72
|
+
*/
|
|
73
|
+
const stripHtml = value => value.replace(new RegExp(ANGLE_BRACKET_ENTITIES, 'gi'), '').replace(/<[^>]*>/g, '').replace(/[<>]/g, '');
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Removes HTML from a single URI component. The component is only modified when it contains
|
|
77
|
+
* suspicious characters, so regular components (e.g. encoded product ids) stay untouched.
|
|
78
|
+
* @param {string} component A single URI encoded component like a path segment.
|
|
79
|
+
* @returns {string}
|
|
80
|
+
*/
|
|
81
|
+
const sanitizeUriComponent = component => {
|
|
82
|
+
let decoded;
|
|
83
|
+
try {
|
|
84
|
+
decoded = decodeURIComponent(component);
|
|
85
|
+
} catch (e) {
|
|
86
|
+
// Malformed encoding - fall back to the raw component.
|
|
87
|
+
decoded = component;
|
|
88
|
+
}
|
|
89
|
+
if (!UNSAFE_CHARS_REGEX.test(decoded)) {
|
|
90
|
+
return component;
|
|
91
|
+
}
|
|
92
|
+
return encodeURIComponent(stripHtml(decoded));
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Removes HTML from a decoded query parameter key or value.
|
|
97
|
+
* @param {string} value The decoded value.
|
|
98
|
+
* @returns {string}
|
|
99
|
+
*/
|
|
100
|
+
const sanitizeQueryValue = value => UNSAFE_CHARS_REGEX.test(value) ? stripHtml(value) : value;
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Sanitizes a link which was received from an external source (e.g. deep links or push messages)
|
|
104
|
+
* before it's passed to the router. HTML markup is removed from the path, query and hash to prevent
|
|
105
|
+
* that it's rendered somewhere within the app. Links with a script protocol are rejected.
|
|
106
|
+
* Links without suspicious content are returned unchanged.
|
|
107
|
+
* @param {string} link The link to sanitize.
|
|
108
|
+
* @returns {string} The sanitized link or an empty string when the link was rejected.
|
|
109
|
+
*/
|
|
110
|
+
export const sanitizeLink = link => {
|
|
111
|
+
if (typeof link !== 'string' || link === '') {
|
|
112
|
+
return '';
|
|
113
|
+
}
|
|
114
|
+
if (UNSAFE_PROTOCOL_REGEX.test(link)) {
|
|
115
|
+
logger.warn('sanitizeLink: Rejected link with unsafe protocol', link);
|
|
116
|
+
return '';
|
|
117
|
+
}
|
|
118
|
+
const hashIndex = link.indexOf('#');
|
|
119
|
+
const beforeHash = hashIndex === -1 ? link : link.slice(0, hashIndex);
|
|
120
|
+
const hash = hashIndex === -1 ? null : link.slice(hashIndex + 1);
|
|
121
|
+
const queryIndex = beforeHash.indexOf('?');
|
|
122
|
+
const base = queryIndex === -1 ? beforeHash : beforeHash.slice(0, queryIndex);
|
|
123
|
+
const query = queryIndex === -1 ? null : beforeHash.slice(queryIndex + 1);
|
|
124
|
+
let sanitized = base.split('/').map(sanitizeUriComponent).join('/');
|
|
125
|
+
if (query !== null) {
|
|
126
|
+
const params = new URLSearchParams(query);
|
|
127
|
+
const sanitizedParams = new URLSearchParams();
|
|
128
|
+
let modified = false;
|
|
129
|
+
params.forEach((value, key) => {
|
|
130
|
+
const sanitizedKey = sanitizeQueryValue(key);
|
|
131
|
+
const sanitizedValue = sanitizeQueryValue(value);
|
|
132
|
+
modified = modified || sanitizedKey !== key || sanitizedValue !== value;
|
|
133
|
+
sanitizedParams.append(sanitizedKey, sanitizedValue);
|
|
134
|
+
});
|
|
135
|
+
sanitized += `?${modified ? sanitizedParams.toString() : query}`;
|
|
136
|
+
}
|
|
137
|
+
if (hash !== null) {
|
|
138
|
+
sanitized += `#${sanitizeUriComponent(hash)}`;
|
|
139
|
+
}
|
|
140
|
+
if (sanitized !== link) {
|
|
141
|
+
logger.warn('sanitizeLink: Removed unsafe content from link', link);
|
|
142
|
+
}
|
|
143
|
+
return sanitized;
|
|
59
144
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@shopgate/pwa-common",
|
|
3
|
-
"version": "7.31.
|
|
3
|
+
"version": "7.31.9-beta.1",
|
|
4
4
|
"description": "Common library for the Shopgate Connect PWA.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"author": "Shopgate <support@shopgate.com>",
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
"dependencies": {
|
|
18
18
|
"@redux-devtools/extension": "^3.3.0",
|
|
19
19
|
"@sentry/browser": "6.0.1",
|
|
20
|
-
"@shopgate/pwa-benchmark": "7.31.
|
|
20
|
+
"@shopgate/pwa-benchmark": "7.31.9-beta.1",
|
|
21
21
|
"@virtuous/conductor": "~2.5.0",
|
|
22
22
|
"@virtuous/react-conductor": "~2.5.0",
|
|
23
23
|
"@virtuous/redux-persister": "1.1.0-beta.7",
|
|
@@ -40,7 +40,7 @@
|
|
|
40
40
|
"swiper": "12.1.0"
|
|
41
41
|
},
|
|
42
42
|
"devDependencies": {
|
|
43
|
-
"@shopgate/pwa-core": "7.31.
|
|
43
|
+
"@shopgate/pwa-core": "7.31.9-beta.1",
|
|
44
44
|
"@types/lodash": "^4.17.24",
|
|
45
45
|
"@types/react-portal": "^3.0.9",
|
|
46
46
|
"lodash": "^4.17.23",
|
package/subscriptions/router.js
CHANGED
|
@@ -21,6 +21,7 @@ import { navigate$, userDidLogin$, appWillStart$, windowOpenOverride$ } from "..
|
|
|
21
21
|
import { isUserLoggedIn } from "../selectors/user";
|
|
22
22
|
import { getIsConnected } from "../selectors/client";
|
|
23
23
|
import { INDEX_PATH } from "../constants/RoutePaths";
|
|
24
|
+
import { sanitizeLink } from "../helpers/router";
|
|
24
25
|
import appConfig from "../helpers/config";
|
|
25
26
|
import authRoutes from "../collections/AuthRoutes";
|
|
26
27
|
import ToastProvider from "../providers/toast";
|
|
@@ -331,9 +332,10 @@ export default function routerSubscriptions(subscribe) {
|
|
|
331
332
|
* so that the router can decide how to handle the URL.
|
|
332
333
|
*/
|
|
333
334
|
Linking.addEventListener('windowOpenRequested', event => {
|
|
334
|
-
const
|
|
335
|
-
|
|
336
|
-
|
|
335
|
+
const targetUrl = sanitizeLink(String(event.detail?.targetUrl || ''));
|
|
336
|
+
if (!targetUrl) {
|
|
337
|
+
return;
|
|
338
|
+
}
|
|
337
339
|
dispatch(historyPush({
|
|
338
340
|
pathname: targetUrl,
|
|
339
341
|
state: {}
|
|
@@ -371,9 +373,10 @@ export default function routerSubscriptions(subscribe) {
|
|
|
371
373
|
action,
|
|
372
374
|
dispatch
|
|
373
375
|
}) => {
|
|
374
|
-
|
|
376
|
+
const pathname = sanitizeLink(String(action.pathname || ''));
|
|
377
|
+
if (pathname) {
|
|
375
378
|
dispatch(historyPush({
|
|
376
|
-
pathname
|
|
379
|
+
pathname
|
|
377
380
|
}));
|
|
378
381
|
}
|
|
379
382
|
});
|