@faststore/core 3.100.4 → 3.100.6
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/.next/BUILD_ID +1 -1
- package/.next/build-manifest.json +2 -2
- package/.next/cache/.tsbuildinfo +1 -1
- package/.next/cache/config.json +3 -3
- package/.next/cache/webpack/client-production/0.pack +2 -2
- package/.next/cache/webpack/client-production/index.pack +0 -0
- package/.next/cache/webpack/edge-server-production/0.pack +0 -0
- package/.next/cache/webpack/edge-server-production/index.pack +0 -0
- package/.next/cache/webpack/server-production/0.pack +2 -2
- package/.next/cache/webpack/server-production/index.pack +0 -0
- package/.next/prerender-manifest.js +1 -1
- package/.next/prerender-manifest.json +1 -1
- package/.next/routes-manifest.json +1 -1
- package/.next/server/functions-config-manifest.json +1 -1
- package/.next/server/middleware-build-manifest.js +1 -1
- package/.next/server/pages/api/graphql.js +3 -3
- package/.next/server/pages/en-US/404.html +2 -2
- package/.next/server/pages/en-US/500.html +2 -2
- package/.next/server/pages/en-US/checkout.html +2 -2
- package/.next/server/pages/en-US/login.html +2 -2
- package/.next/server/pages/en-US/password-protection.html +2 -2
- package/.next/server/pages/en-US/s.html +2 -2
- package/.next/server/pages/en-US.html +2 -2
- package/.next/server/pages-manifest.json +1 -1
- package/.next/trace +156 -156
- package/.turbo/turbo-build.log +11 -11
- package/.turbo/turbo-test.log +10 -9
- package/CHANGELOG.md +12 -0
- package/package.json +2 -2
- package/src/pages/api/graphql.ts +40 -26
- package/src/utils/trustedForwardedHost.ts +50 -0
- package/test/utils/trustedForwardedHost.test.ts +103 -0
- /package/.next/static/{s20QxDT2pZcGbsFUk-djq → TfWW2xib3Snr8xF4Ldqws}/_buildManifest.js +0 -0
- /package/.next/static/{s20QxDT2pZcGbsFUk-djq → TfWW2xib3Snr8xF4Ldqws}/_ssgManifest.js +0 -0
package/.turbo/turbo-build.log
CHANGED
|
@@ -1,23 +1,23 @@
|
|
|
1
1
|
|
|
2
|
-
> @faststore/core@3.100.
|
|
2
|
+
> @faststore/core@3.100.4 prebuild /home/runner/work/faststore/faststore/packages/core
|
|
3
3
|
> na run partytown && na run generate
|
|
4
4
|
|
|
5
5
|
|
|
6
|
-
> @faststore/core@3.100.
|
|
6
|
+
> @faststore/core@3.100.4 partytown /home/runner/work/faststore/faststore/packages/core
|
|
7
7
|
> partytown copylib ./public/~partytown
|
|
8
8
|
|
|
9
9
|
Partytown lib copied to: /home/runner/work/faststore/faststore/packages/core/public/~partytown
|
|
10
10
|
|
|
11
|
-
> @faststore/core@3.100.
|
|
11
|
+
> @faststore/core@3.100.4 generate /home/runner/work/faststore/faststore/packages/core
|
|
12
12
|
> na run generate:schema && na run generate:codegen && na run format:generated
|
|
13
13
|
|
|
14
14
|
|
|
15
|
-
> @faststore/core@3.100.
|
|
15
|
+
> @faststore/core@3.100.4 generate:schema /home/runner/work/faststore/faststore/packages/core
|
|
16
16
|
> tsx src/server/generator/generateGraphQLSchemaFile.ts
|
|
17
17
|
|
|
18
18
|
Schema GraphQL file generated successfully
|
|
19
19
|
|
|
20
|
-
> @faststore/core@3.100.
|
|
20
|
+
> @faststore/core@3.100.4 generate:codegen /home/runner/work/faststore/faststore/packages/core
|
|
21
21
|
> graphql-codegen
|
|
22
22
|
|
|
23
23
|
[STARTED] Parse Configuration
|
|
@@ -37,11 +37,11 @@ Running lifecycle hook "afterStart" scripts...
|
|
|
37
37
|
[CLI] Loading Documents
|
|
38
38
|
[CLI] Generating output
|
|
39
39
|
|
|
40
|
-
> @faststore/core@3.100.
|
|
40
|
+
> @faststore/core@3.100.4 format:generated /home/runner/work/faststore/faststore/packages/core
|
|
41
41
|
> prettier --write "@generated/**/*.{ts,js,tsx,jsx,json}" --loglevel error
|
|
42
42
|
|
|
43
43
|
|
|
44
|
-
> @faststore/core@3.100.
|
|
44
|
+
> @faststore/core@3.100.4 build /home/runner/work/faststore/faststore/packages/core
|
|
45
45
|
> next build
|
|
46
46
|
|
|
47
47
|
⚠ No build cache found. Please configure build caching for faster rebuilds. Read more: https://nextjs.org/docs/messages/no-cache
|
|
@@ -58,7 +58,7 @@ Browserslist: caniuse-lite is outdated. Please run:
|
|
|
58
58
|
Creating an optimized production build ...
|
|
59
59
|
Disabled SWC as replacement for Babel because of custom Babel configuration ".babelrc.js" https://nextjs.org/docs/messages/swc-disabled
|
|
60
60
|
Using external babel configuration from /home/runner/work/faststore/faststore/packages/core/.babelrc.js
|
|
61
|
-
Browserslist: browsers data (caniuse-lite) is
|
|
61
|
+
Browserslist: browsers data (caniuse-lite) is 21 months old. Please run:
|
|
62
62
|
npx update-browserslist-db@latest
|
|
63
63
|
Why you should do it regularly: https://github.com/browserslist/update-db#readme
|
|
64
64
|
<w> [webpack.cache.PackFileCacheStrategy] Skipped not serializable cache item 'Compilation/modules|/home/runner/work/faststore/faststore/node_modules/.pnpm/next@13.5.11_@babel+core@7.26.7_@opentelemetry+api@1.4.1_react-dom@18.3.1_react@18.3.1__react@18.3.1_sass@1.83.4/node_modules/next/dist/build/webpack/loaders/css-loader/src/index.js??ruleSet[1].rules[7].oneOf[9].use[1]!/home/runner/work/faststore/faststore/node_modules/.pnpm/next@13.5.11_@babel+core@7.26.7_@opentelemetry+api@1.4.1_react-dom@18.3.1_react@18.3.1__react@18.3.1_sass@1.83.4/node_modules/next/dist/build/webpack/loaders/postcss-loader/src/index.js??ruleSet[1].rules[7].oneOf[9].use[2]!/home/runner/work/faststore/faststore/node_modules/.pnpm/next@13.5.11_@babel+core@7.26.7_@opentelemetry+api@1.4.1_react-dom@18.3.1_react@18.3.1__react@18.3.1_sass@1.83.4/node_modules/next/dist/build/webpack/loaders/resolve-url-loader/index.js??ruleSet[1].rules[7].oneOf[9].use[3]!/home/runner/work/faststore/faststore/node_modules/.pnpm/next@13.5.11_@babel+core@7.26.7_@opentelemetry+api@1.4.1_react-dom@18.3.1_react@18.3.1__react@18.3.1_sass@1.83.4/node_modules/next/dist/compiled/sass-loader/cjs.js??ruleSet[1].rules[7].oneOf[9].use[4]!/home/runner/work/faststore/faststore/packages/core/src/components/common/Toast/section.module.scss': No serializer registered for Warning
|
|
@@ -83,7 +83,7 @@ Learn more: https://nextjs.org/docs/api-reference/edge-runtime
|
|
|
83
83
|
|
|
84
84
|
Import trace for requested module:
|
|
85
85
|
../../node_modules/.pnpm/jose@6.2.3/node_modules/jose/dist/webapi/lib/deflate.js
|
|
86
|
-
../../node_modules/.pnpm/jose@6.2.3/node_modules/jose/dist/webapi/jwe/flattened/
|
|
86
|
+
../../node_modules/.pnpm/jose@6.2.3/node_modules/jose/dist/webapi/jwe/flattened/decrypt.js
|
|
87
87
|
../../node_modules/.pnpm/jose@6.2.3/node_modules/jose/dist/webapi/index.js
|
|
88
88
|
./src/server/password-protection-service.ts
|
|
89
89
|
|
|
@@ -93,7 +93,7 @@ Learn more: https://nextjs.org/docs/api-reference/edge-runtime
|
|
|
93
93
|
|
|
94
94
|
Import trace for requested module:
|
|
95
95
|
../../node_modules/.pnpm/jose@6.2.3/node_modules/jose/dist/webapi/lib/deflate.js
|
|
96
|
-
../../node_modules/.pnpm/jose@6.2.3/node_modules/jose/dist/webapi/jwe/flattened/
|
|
96
|
+
../../node_modules/.pnpm/jose@6.2.3/node_modules/jose/dist/webapi/jwe/flattened/decrypt.js
|
|
97
97
|
../../node_modules/.pnpm/jose@6.2.3/node_modules/jose/dist/webapi/index.js
|
|
98
98
|
./src/server/password-protection-service.ts
|
|
99
99
|
|
|
@@ -101,8 +101,8 @@ Import trace for requested module:
|
|
|
101
101
|
Generating static pages (0/8) ...
|
|
102
102
|
|
|
103
103
|
Generating static pages (2/8)
|
|
104
104
|
Search not found. Add a new component for this section or remove it from the CMS
|
|
105
|
-
Search not found. Add a new component for this section or remove it from the CMS
|
|
106
105
|
Warning: Dynamic Content not found for the page: home. Refer to the Dynamic Content documentation at https://developers.vtex.com/docs/guides/faststore/dynamic-content-overview for mapping the page and the corresponding data-fetching function.
|
|
106
|
+
Search not found. Add a new component for this section or remove it from the CMS
|
|
107
107
|
|
|
108
108
|
Generating static pages (4/8)
|
|
109
109
|
Search not found. Add a new component for this section or remove it from the CMS
|
|
110
110
|
Search not found. Add a new component for this section or remove it from the CMS
|
package/.turbo/turbo-test.log
CHANGED
|
@@ -1,21 +1,22 @@
|
|
|
1
1
|
|
|
2
|
-
> @faststore/core@3.100.
|
|
2
|
+
> @faststore/core@3.100.4 test /home/runner/work/faststore/faststore/packages/core
|
|
3
3
|
> jest
|
|
4
4
|
|
|
5
|
-
PASS test/utils/clearCookies.test.ts (
|
|
6
|
-
PASS test/
|
|
5
|
+
PASS test/utils/clearCookies.test.ts (13.987 s)
|
|
6
|
+
PASS test/server/password-protection-service.test.ts (18.623 s)
|
|
7
|
+
PASS test/utils/cookieCacheBusting.test.ts (16.597 s)
|
|
7
8
|
PASS test/utils/multipleTemplates.test.ts
|
|
8
|
-
PASS test/server/password-protection-service.test.ts (16.602 s)
|
|
9
9
|
PASS test/sdk/redirects/index.test.ts
|
|
10
|
+
PASS test/utils/trustedForwardedHost.test.ts
|
|
10
11
|
PASS test/utils/validateSessionRefreshToken.test.ts
|
|
11
12
|
PASS test/server/cms/global.test.ts
|
|
12
13
|
PASS test/utils/isLocalHost.test.ts
|
|
13
14
|
PASS test/utils/isLocalHost.browser.test.ts
|
|
14
|
-
PASS test/server/cms/index.test.ts (5.
|
|
15
|
-
PASS test/server/index.test.ts (10.
|
|
15
|
+
PASS test/server/cms/index.test.ts (5.82 s)
|
|
16
|
+
PASS test/server/index.test.ts (10.891 s)
|
|
16
17
|
|
|
17
|
-
Test Suites:
|
|
18
|
-
Tests:
|
|
18
|
+
Test Suites: 12 passed, 12 total
|
|
19
|
+
Tests: 119 passed, 119 total
|
|
19
20
|
Snapshots: 0 total
|
|
20
|
-
Time:
|
|
21
|
+
Time: 30.388 s
|
|
21
22
|
Ran all test suites.
|
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,18 @@
|
|
|
3
3
|
All notable changes to this project will be documented in this file.
|
|
4
4
|
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
|
5
5
|
|
|
6
|
+
## [3.100.6](https://github.com/vtex/faststore/compare/v3.100.5...v3.100.6) (2026-10-07)
|
|
7
|
+
|
|
8
|
+
### Bug Fixes
|
|
9
|
+
|
|
10
|
+
- address security & functional concerns from dk review (3.x backport) ([c928a1f](https://github.com/vtex/faststore/commit/c928a1fc04bc3554d83302c4cf760adab0f7e81c))
|
|
11
|
+
- **core:** harden x-forwarded-host handling and drop orderNumber fallback ([55990c1](https://github.com/vtex/faststore/commit/55990c1c6208c06ae482e71816df2035292bf2e9)), closes [#3178](https://github.com/vtex/faststore/issues/3178)
|
|
12
|
+
- **core:** ignore multi-value x-forwarded-host and cover port and case ([81ba26b](https://github.com/vtex/faststore/commit/81ba26b93175878b9cc3e32683f9c13b0abf25e9))
|
|
13
|
+
- **core:** log ignored x-forwarded-host and cover CR/LF hostnames ([e6a326f](https://github.com/vtex/faststore/commit/e6a326f6d0d4a91c75b9f0a10abfdc7c22c897be))
|
|
14
|
+
- **core:** make preview cookies host-only instead of rewriting their domain ([989fc69](https://github.com/vtex/faststore/commit/989fc69c573c980e354d11e3c3d8a719e2833fb2))
|
|
15
|
+
- **core:** reject hostnames that would inject Set-Cookie attributes ([109d819](https://github.com/vtex/faststore/commit/109d81917e1200dd78b9500f4cb81d128c78003d))
|
|
16
|
+
- resolve ValidateCartMutation infinite loop in preview environments (SFS-3034) ([ad7800d](https://github.com/vtex/faststore/commit/ad7800d65493c7477c9fb30119f76925647c40fc)), closes [#3178](https://github.com/vtex/faststore/issues/3178) [#3178](https://github.com/vtex/faststore/issues/3178) [#3190](https://github.com/vtex/faststore/issues/3190) [#3188](https://github.com/vtex/faststore/issues/3188) [#1467584](https://github.com/vtex/faststore/issues/1467584)
|
|
17
|
+
|
|
6
18
|
## [3.100.4](https://github.com/vtex/faststore/compare/v3.100.3...v3.100.4) (2026-08-28)
|
|
7
19
|
|
|
8
20
|
### Bug Fixes
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@faststore/core",
|
|
3
|
-
"version": "3.100.
|
|
3
|
+
"version": "3.100.6",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -127,5 +127,5 @@
|
|
|
127
127
|
"ts-jest": "29.1.1",
|
|
128
128
|
"typescript": "5.3.2"
|
|
129
129
|
},
|
|
130
|
-
"gitHead": "
|
|
130
|
+
"gitHead": "31df58321c484732bd4a69618e9eb09e6a2c0261"
|
|
131
131
|
}
|
package/src/pages/api/graphql.ts
CHANGED
|
@@ -10,6 +10,11 @@ import type { NextApiHandler, NextApiRequest } from 'next'
|
|
|
10
10
|
import discoveryConfig from 'discovery.config'
|
|
11
11
|
import { getJWTAutCookie } from 'src/utils/getCookie'
|
|
12
12
|
import { isLocalHost } from 'src/utils/isLocalHost'
|
|
13
|
+
import {
|
|
14
|
+
isHostAllowed,
|
|
15
|
+
removeCookieDomain,
|
|
16
|
+
singleForwardedHost,
|
|
17
|
+
} from 'src/utils/trustedForwardedHost'
|
|
13
18
|
import { shouldForceRefreshTokenForValidateSession } from 'src/utils/validateSessionRefreshToken'
|
|
14
19
|
import { execute } from '../../server'
|
|
15
20
|
|
|
@@ -20,24 +25,45 @@ const ALLOWED_HOST_SUFFIXES = ['localhost', '.vtex.app', '.localhost']
|
|
|
20
25
|
// Example: "Set-Cookie: key=value; Domain=example.com; Path=/"
|
|
21
26
|
const MATCH_DOMAIN_REGEXP = /(?:^|;\s*)(?:domain=)([^;]+)/i
|
|
22
27
|
|
|
28
|
+
const toHostname = (hostHeader: string | undefined) => {
|
|
29
|
+
const trimmed = hostHeader?.trim()
|
|
30
|
+
if (!trimmed) {
|
|
31
|
+
return null
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
try {
|
|
35
|
+
return new URL(`https://${trimmed}`).hostname
|
|
36
|
+
} catch {
|
|
37
|
+
return null
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
23
41
|
/**
|
|
24
|
-
* Extracts hostname from the incoming request.
|
|
42
|
+
* Extracts hostname from the incoming request. The preview ingress forwards a
|
|
43
|
+
* client-supplied x-forwarded-host as is, so it only decides whether the cookie
|
|
44
|
+
* Domain is dropped, never which domain is used. A multi-value header is
|
|
45
|
+
* ignored; a single value is used only if it is on the allowlist.
|
|
25
46
|
*/
|
|
26
47
|
const getRequestHostname = ({
|
|
27
48
|
request,
|
|
28
49
|
}: {
|
|
29
50
|
request: NextApiRequest
|
|
30
51
|
}): string | null => {
|
|
31
|
-
const
|
|
32
|
-
|
|
33
|
-
|
|
52
|
+
const forwardedHeader = request.headers['x-forwarded-host']
|
|
53
|
+
const forwardedHostname = toHostname(singleForwardedHost(forwardedHeader))
|
|
54
|
+
if (forwardedHeader && !forwardedHostname) {
|
|
55
|
+
// The value is client-controllable, so it is not logged.
|
|
56
|
+
console.warn('Ignoring multi-value or malformed x-forwarded-host')
|
|
34
57
|
}
|
|
35
58
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
59
|
+
if (
|
|
60
|
+
forwardedHostname &&
|
|
61
|
+
isHostAllowed(forwardedHostname, ALLOWED_HOST_SUFFIXES)
|
|
62
|
+
) {
|
|
63
|
+
return forwardedHostname
|
|
40
64
|
}
|
|
65
|
+
|
|
66
|
+
return toHostname(request.headers.host)
|
|
41
67
|
}
|
|
42
68
|
|
|
43
69
|
/**
|
|
@@ -57,22 +83,10 @@ const shouldReplaceCookieDomain = ({
|
|
|
57
83
|
}
|
|
58
84
|
|
|
59
85
|
/**
|
|
60
|
-
*
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
allowList,
|
|
65
|
-
}: {
|
|
66
|
-
host: string
|
|
67
|
-
allowList: string[]
|
|
68
|
-
}) => {
|
|
69
|
-
const normalizedHost = host.toLowerCase()
|
|
70
|
-
|
|
71
|
-
return allowList.some((suffix) => normalizedHost.endsWith(suffix))
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
/**
|
|
75
|
-
* Ensure the cookie domain matches the current host so the browser can store it.
|
|
86
|
+
* On allowlisted hosts (previews, localhost) the upstream cookie domain does not
|
|
87
|
+
* match the browser host, so the Domain attribute is dropped and the cookie
|
|
88
|
+
* becomes host-only. The host is never written into the cookie, so a forged
|
|
89
|
+
* x-forwarded-host cannot choose the cookie scope.
|
|
76
90
|
*/
|
|
77
91
|
const normalizeSetCookieDomain = ({
|
|
78
92
|
request,
|
|
@@ -93,13 +107,13 @@ const normalizeSetCookieDomain = ({
|
|
|
93
107
|
const cookieDomain = domainMatch[1]
|
|
94
108
|
|
|
95
109
|
if (
|
|
96
|
-
!
|
|
110
|
+
!isHostAllowed(host, ALLOWED_HOST_SUFFIXES) ||
|
|
97
111
|
!shouldReplaceCookieDomain({ cookieDomain, host })
|
|
98
112
|
) {
|
|
99
113
|
return setCookie
|
|
100
114
|
}
|
|
101
115
|
|
|
102
|
-
return setCookie
|
|
116
|
+
return removeCookieDomain(setCookie)
|
|
103
117
|
}
|
|
104
118
|
|
|
105
119
|
const parseRequest = (request: NextApiRequest) => {
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
// The result is written verbatim into a Set-Cookie Domain attribute, so
|
|
2
|
+
// characters such as ";" or spaces would inject cookie attributes.
|
|
3
|
+
const HOSTNAME_PATTERN = /^[a-z0-9.-]+$/
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* An entry starting with "." matches subdomains only; any other entry matches
|
|
7
|
+
* the host itself and its subdomains. Matching is anchored on a label boundary,
|
|
8
|
+
* so "evil-localhost" never matches "localhost".
|
|
9
|
+
*/
|
|
10
|
+
export function isHostAllowed(host: string, allowList: string[]) {
|
|
11
|
+
const normalizedHost = host.toLowerCase()
|
|
12
|
+
|
|
13
|
+
if (!HOSTNAME_PATTERN.test(normalizedHost)) {
|
|
14
|
+
return false
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
return allowList.some((entry) =>
|
|
18
|
+
entry.startsWith('.')
|
|
19
|
+
? normalizedHost.endsWith(entry)
|
|
20
|
+
: normalizedHost === entry || normalizedHost.endsWith(`.${entry}`)
|
|
21
|
+
)
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Returns the `x-forwarded-host` value only when exactly one is present.
|
|
26
|
+
*
|
|
27
|
+
* Which hop of a proxy chain is trustworthy is not known here: with several
|
|
28
|
+
* values the leftmost one is whatever the client sent. So a multi-value header
|
|
29
|
+
* (comma-joined by Node, or an array) is ignored and the caller falls back to
|
|
30
|
+
* `host`. A single value still assumes the ingress overwrites the header.
|
|
31
|
+
*/
|
|
32
|
+
export function singleForwardedHost(
|
|
33
|
+
header: string | string[] | undefined
|
|
34
|
+
): string | undefined {
|
|
35
|
+
if (Array.isArray(header)) {
|
|
36
|
+
return header.length === 1 ? singleForwardedHost(header[0]) : undefined
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const value = header?.trim()
|
|
40
|
+
|
|
41
|
+
return value && !value.includes(',') ? value : undefined
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Drops every Domain attribute so the cookie becomes host-only: the browser
|
|
46
|
+
* scopes it to the exact host it requested, whatever header picked this path.
|
|
47
|
+
*/
|
|
48
|
+
export function removeCookieDomain(setCookie: string): string {
|
|
49
|
+
return setCookie.replaceAll(/;\s*domain=[^;]*/gi, '')
|
|
50
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import {
|
|
2
|
+
singleForwardedHost,
|
|
3
|
+
isHostAllowed,
|
|
4
|
+
removeCookieDomain,
|
|
5
|
+
} from '../../src/utils/trustedForwardedHost'
|
|
6
|
+
|
|
7
|
+
const ALLOW_LIST = ['localhost', '.vtex.app', '.localhost']
|
|
8
|
+
|
|
9
|
+
describe('isHostAllowed', () => {
|
|
10
|
+
it.each([
|
|
11
|
+
'localhost',
|
|
12
|
+
'store.vtex.app',
|
|
13
|
+
'sfj-1--acc.preview.vtex.app',
|
|
14
|
+
'a.localhost',
|
|
15
|
+
])('allows %s', (host: string) => {
|
|
16
|
+
expect(isHostAllowed(host, ALLOW_LIST)).toBe(true)
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
it.each([
|
|
20
|
+
'evil-localhost',
|
|
21
|
+
'notlocalhost',
|
|
22
|
+
'evil-vtex.app',
|
|
23
|
+
'vtex.app.evil.com',
|
|
24
|
+
'evil.com',
|
|
25
|
+
])('rejects %s', (host: string) => {
|
|
26
|
+
expect(isHostAllowed(host, ALLOW_LIST)).toBe(false)
|
|
27
|
+
})
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
describe('isHostAllowed normalization', () => {
|
|
31
|
+
it('is case-insensitive', () => {
|
|
32
|
+
expect(isHostAllowed('Store.VTEX.app', ALLOW_LIST)).toBe(true)
|
|
33
|
+
expect(isHostAllowed('LOCALHOST', ALLOW_LIST)).toBe(true)
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
it.each([
|
|
37
|
+
'x;max-age=0;y.vtex.app',
|
|
38
|
+
'a b.vtex.app',
|
|
39
|
+
'x"y.vtex.app',
|
|
40
|
+
'x=y.vtex.app',
|
|
41
|
+
'x,y.vtex.app',
|
|
42
|
+
'x\r\nSet-Cookie: evil=1.vtex.app',
|
|
43
|
+
'x\ny.vtex.app',
|
|
44
|
+
'x\ry.vtex.app',
|
|
45
|
+
])('rejects %s, which would inject cookie attributes', (host: string) => {
|
|
46
|
+
expect(isHostAllowed(host, ALLOW_LIST)).toBe(false)
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
it('accepts punycode hostnames', () => {
|
|
50
|
+
expect(isHostAllowed('xn--lja-bma.vtex.app', ALLOW_LIST)).toBe(true)
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
it('expects a hostname: a port is not stripped here', () => {
|
|
54
|
+
expect(isHostAllowed('store.vtex.app:8080', ALLOW_LIST)).toBe(false)
|
|
55
|
+
expect(isHostAllowed('evil.com:vtex.app', ALLOW_LIST)).toBe(false)
|
|
56
|
+
})
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
describe('singleForwardedHost', () => {
|
|
60
|
+
it('returns the value when there is exactly one', () => {
|
|
61
|
+
expect(singleForwardedHost('a.vtex.app')).toBe('a.vtex.app')
|
|
62
|
+
expect(singleForwardedHost(' a.vtex.app ')).toBe('a.vtex.app')
|
|
63
|
+
expect(singleForwardedHost(['a.vtex.app'])).toBe('a.vtex.app')
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
it('returns undefined when missing or empty', () => {
|
|
67
|
+
expect(singleForwardedHost(undefined)).toBeUndefined()
|
|
68
|
+
expect(singleForwardedHost('')).toBeUndefined()
|
|
69
|
+
expect(singleForwardedHost([])).toBeUndefined()
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
it('ignores multi-value headers instead of picking one', () => {
|
|
73
|
+
expect(singleForwardedHost('a.vtex.app, evil.com')).toBeUndefined()
|
|
74
|
+
expect(singleForwardedHost('evil.com, a.vtex.app')).toBeUndefined()
|
|
75
|
+
expect(singleForwardedHost(['a.vtex.app', 'evil.com'])).toBeUndefined()
|
|
76
|
+
})
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
describe('removeCookieDomain', () => {
|
|
80
|
+
it('drops the Domain attribute and keeps the rest', () => {
|
|
81
|
+
expect(
|
|
82
|
+
removeCookieDomain(
|
|
83
|
+
'checkout.vtex.com=__ofid=abc; expires=Sun, 04 Apr 2027 18:31:26 GMT; domain=acc.vtexcommercestable.com.br; path=/; secure; samesite=lax; httponly'
|
|
84
|
+
)
|
|
85
|
+
).toBe(
|
|
86
|
+
'checkout.vtex.com=__ofid=abc; expires=Sun, 04 Apr 2027 18:31:26 GMT; path=/; secure; samesite=lax; httponly'
|
|
87
|
+
)
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
it('is case-insensitive and removes repeated Domain attributes', () => {
|
|
91
|
+
expect(removeCookieDomain('a=1; Domain=x.com; DOMAIN=y.com; Path=/')).toBe(
|
|
92
|
+
'a=1; Path=/'
|
|
93
|
+
)
|
|
94
|
+
})
|
|
95
|
+
|
|
96
|
+
it('does not touch a cookie whose name is domain', () => {
|
|
97
|
+
expect(removeCookieDomain('domain=1; path=/')).toBe('domain=1; path=/')
|
|
98
|
+
})
|
|
99
|
+
|
|
100
|
+
it('keeps cookies without a Domain attribute unchanged', () => {
|
|
101
|
+
expect(removeCookieDomain('a=1; path=/')).toBe('a=1; path=/')
|
|
102
|
+
})
|
|
103
|
+
})
|
|
File without changes
|
|
File without changes
|