@salesforce/ui-bundle-template-feature-react-authentication 11.66.2 → 11.66.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.
package/dist/CHANGELOG.md CHANGED
@@ -3,6 +3,22 @@
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
+ ## [11.66.4](https://github.com/salesforce-experience-platform-emu/webapps/compare/v11.66.3...v11.66.4) (2026-08-20)
7
+
8
+ **Note:** Version bump only for package @salesforce/ui-bundle-template-base-sfdx-project
9
+
10
+
11
+
12
+
13
+
14
+ ## [11.66.3](https://github.com/salesforce-experience-platform-emu/webapps/compare/v11.66.2...v11.66.3) (2026-08-20)
15
+
16
+ **Note:** Version bump only for package @salesforce/ui-bundle-template-base-sfdx-project
17
+
18
+
19
+
20
+
21
+
6
22
  ## [11.66.2](https://github.com/salesforce-experience-platform-emu/webapps/compare/v11.66.1...v11.66.2) (2026-08-20)
7
23
 
8
24
  **Note:** Version bump only for package @salesforce/ui-bundle-template-base-sfdx-project
@@ -0,0 +1,141 @@
1
+ /**
2
+ * Test class for UIBundleAuthUtils.
3
+ *
4
+ * Focuses on getSanitizedStartUrl, the shared open-redirect (CWE-601) guard
5
+ * used by UIBundleLogin, UIBundleRegistration and UIBundleSocialLoginConfig.
6
+ * Every rejection path and the safe-path prefixing
7
+ * behavior are asserted deterministically with an explicit path prefix.
8
+ */
9
+ @isTest
10
+ private class UIBundleAuthUtils_Test {
11
+
12
+ // Explicit, non-null community path prefix so behavior is deterministic
13
+ // (Site.getPathPrefix() is null/blank in a plain unit-test context).
14
+ private static final String PREFIX = '/community';
15
+
16
+ // ---- Safe / allowed inputs ----------------------------------------------
17
+
18
+ @isTest
19
+ static void allowsSafeRelativePath() {
20
+ System.assertEquals(
21
+ '/community/dashboard',
22
+ UIBundleAuthUtils.getSanitizedStartUrl('/dashboard', PREFIX),
23
+ 'A safe community-relative path should be prefixed and preserved'
24
+ );
25
+ }
26
+
27
+ @isTest
28
+ static void allowsNestedRelativePath() {
29
+ System.assertEquals(
30
+ '/community/safe/relative/path',
31
+ UIBundleAuthUtils.getSanitizedStartUrl('/safe/relative/path', PREFIX),
32
+ 'A nested relative path should be prefixed and preserved'
33
+ );
34
+ }
35
+
36
+ @isTest
37
+ static void blankAndRootFallBackToDefault() {
38
+ System.assertEquals(PREFIX, UIBundleAuthUtils.getSanitizedStartUrl(null, PREFIX), 'null -> default');
39
+ System.assertEquals(PREFIX, UIBundleAuthUtils.getSanitizedStartUrl('', PREFIX), 'empty -> default');
40
+ System.assertEquals(PREFIX, UIBundleAuthUtils.getSanitizedStartUrl(' ', PREFIX), 'whitespace -> default');
41
+ System.assertEquals(PREFIX, UIBundleAuthUtils.getSanitizedStartUrl('/', PREFIX), '"/" -> default');
42
+ }
43
+
44
+ // ---- Rejected / malicious inputs (all fall back to the default) ----------
45
+
46
+ @isTest
47
+ static void rejectsAbsoluteHttpUrl() {
48
+ System.assertEquals(
49
+ PREFIX,
50
+ UIBundleAuthUtils.getSanitizedStartUrl('https://evil-attacker.com', PREFIX),
51
+ 'Absolute http(s) URL must be rejected'
52
+ );
53
+ }
54
+
55
+ @isTest
56
+ static void rejectsProtocolRelativeUrl() {
57
+ System.assertEquals(
58
+ PREFIX,
59
+ UIBundleAuthUtils.getSanitizedStartUrl('//evil-attacker.com', PREFIX),
60
+ 'Protocol-relative URL must be rejected'
61
+ );
62
+ }
63
+
64
+ @isTest
65
+ static void rejectsAmbiguousScheme() {
66
+ System.assertEquals(
67
+ PREFIX,
68
+ UIBundleAuthUtils.getSanitizedStartUrl('https:evil-attacker.com', PREFIX),
69
+ 'Malformed/ambiguous scheme must be rejected'
70
+ );
71
+ }
72
+
73
+ @isTest
74
+ static void rejectsBackslashVariant() {
75
+ System.assertEquals(
76
+ PREFIX,
77
+ UIBundleAuthUtils.getSanitizedStartUrl('/\\evil-attacker.com', PREFIX),
78
+ 'Backslash normalization trick must be rejected'
79
+ );
80
+ }
81
+
82
+ @isTest
83
+ static void rejectsAtSign() {
84
+ System.assertEquals(
85
+ PREFIX,
86
+ UIBundleAuthUtils.getSanitizedStartUrl('/x@evil-attacker.com', PREFIX),
87
+ 'User-info "@" must be rejected'
88
+ );
89
+ }
90
+
91
+ @isTest
92
+ static void rejectsColon() {
93
+ System.assertEquals(
94
+ PREFIX,
95
+ UIBundleAuthUtils.getSanitizedStartUrl('/path:8080', PREFIX),
96
+ 'Colon (port/scheme manipulation) must be rejected'
97
+ );
98
+ }
99
+
100
+ @isTest
101
+ static void rejectsEncodedProtocolRelativeBypass() {
102
+ // %2f%2f decodes to // — the decode step must catch this.
103
+ System.assertEquals(
104
+ PREFIX,
105
+ UIBundleAuthUtils.getSanitizedStartUrl('%2f%2fevil-attacker.com', PREFIX),
106
+ 'URL-encoded protocol-relative bypass must be rejected'
107
+ );
108
+ }
109
+
110
+ @isTest
111
+ static void rejectsControlCharacters() {
112
+ String withNewline = '/foo' + String.fromCharArray(new List<Integer>{ 10 }) + 'bar';
113
+ System.assertEquals(
114
+ PREFIX,
115
+ UIBundleAuthUtils.getSanitizedStartUrl(withNewline, PREFIX),
116
+ 'Embedded control characters must be rejected'
117
+ );
118
+ }
119
+
120
+ // ---- Supporting API coverage --------------------------------------------
121
+
122
+ @isTest
123
+ static void authExceptionCarriesStatusAndMessages() {
124
+ UIBundleAuthUtils.AuthException single = new UIBundleAuthUtils.AuthException(400, 'bad request');
125
+ System.assertEquals(400, single.statusCode, 'statusCode should round-trip');
126
+ System.assertEquals(1, single.messages.size(), 'single-message constructor');
127
+
128
+ UIBundleAuthUtils.AuthException multi =
129
+ new UIBundleAuthUtils.AuthException(500, new List<String>{ 'a', 'b' });
130
+ System.assertEquals(500, multi.statusCode, 'statusCode should round-trip');
131
+ System.assertEquals(2, multi.messages.size(), 'multi-message constructor');
132
+ }
133
+
134
+ @isTest
135
+ static void debugLogCompletesWithoutError() {
136
+ Test.startTest();
137
+ UIBundleAuthUtils.debugLog(new IllegalArgumentException('boom'), LoggingLevel.ERROR);
138
+ Test.stopTest();
139
+ System.assert(true, 'debugLog should complete without throwing');
140
+ }
141
+ }
@@ -0,0 +1,5 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <ApexClass xmlns="http://soap.sforce.com/2006/04/metadata">
3
+ <apiVersion>67.0</apiVersion>
4
+ <status>Active</status>
5
+ </ApexClass>
@@ -22,17 +22,9 @@ global with sharing class UIBundleSocialLoginConfig {
22
22
  @HttpGet
23
23
  global static SocialLoginConfigResponse getConfig() {
24
24
  try {
25
- String startUrl = RestContext.request.params.get('startURL');
26
- if (String.isBlank(startUrl)) {
27
- startUrl = '/';
28
- }
29
-
30
- // Ensure startURL includes the community path prefix so post-auth
31
- // redirect lands on the correct community (not the org default).
32
- String pathPrefix = Site.getPathPrefix();
33
- if (String.isNotBlank(pathPrefix) && !startUrl.startsWith(pathPrefix)) {
34
- startUrl = pathPrefix + startUrl;
35
- }
25
+ // Resolve the post-authentication redirect path, sanitized against
26
+ // open-redirect attacks (CWE-601). See resolveStartUrl.
27
+ String startUrl = resolveStartUrl();
36
28
 
37
29
  String communityUrl = Site.getBaseUrl();
38
30
  Auth.AuthConfiguration authConfig = new Auth.AuthConfiguration(communityUrl, startUrl);
@@ -89,6 +81,26 @@ global with sharing class UIBundleSocialLoginConfig {
89
81
  }
90
82
  }
91
83
 
84
+ /**
85
+ * Resolves the post-authentication redirect path from the request and
86
+ * sanitizes it to prevent open-redirect attacks (CWE-601).
87
+ *
88
+ * Delegates to the shared UIBundleAuthUtils.getSanitizedStartUrl — the same
89
+ * validation used by UIBundleLogin and UIBundleRegistration — which
90
+ * URL-decodes the value (catching encoded bypasses such as %2f%2f) and then
91
+ * rejects absolute, protocol-relative ("//"), backslash, "@", ":" and
92
+ * control-character inputs, falling back to the community path prefix. Only
93
+ * safe, community-relative paths survive, prefixed with Site.getPathPrefix()
94
+ * so the redirect lands on this community rather than the org default.
95
+ */
96
+ @TestVisible
97
+ private static String resolveStartUrl() {
98
+ return UIBundleAuthUtils.getSanitizedStartUrl(
99
+ RestContext.request.params.get('startURL'),
100
+ Site.getPathPrefix()
101
+ );
102
+ }
103
+
92
104
  global class SocialLoginConfigResponse {
93
105
  global List<AuthProviderInfo> authProviders;
94
106
  global List<SamlProviderInfo> samlProviders;
@@ -98,4 +98,84 @@ private class UIBundleSocialLoginConfig_Test {
98
98
 
99
99
  Test.stopTest();
100
100
  }
101
+
102
+ // ---- Open-redirect sanitization (CWE-601) -------------------------------
103
+ // resolveStartUrl() is the boundary where the endpoint sanitizes the
104
+ // caller-supplied startURL before it is embedded in the SSO redirect URL.
105
+ // These tests assert malicious values never survive that boundary and that
106
+ // legitimate relative paths do.
107
+
108
+ @isTest
109
+ static void resolveStartUrlRejectsAbsoluteUrl() {
110
+ assertNotReflected('https://evil-attacker.com');
111
+ }
112
+
113
+ @isTest
114
+ static void resolveStartUrlRejectsProtocolRelativeUrl() {
115
+ assertNotReflected('//evil-attacker.com');
116
+ }
117
+
118
+ @isTest
119
+ static void resolveStartUrlRejectsBackslashVariant() {
120
+ assertNotReflected('/\\evil-attacker.com');
121
+ }
122
+
123
+ @isTest
124
+ static void resolveStartUrlRejectsEncodedBypass() {
125
+ assertNotReflected('%2f%2fevil-attacker.com');
126
+ }
127
+
128
+ @isTest
129
+ static void resolveStartUrlAllowsRelativePath() {
130
+ setRequest('/dashboard');
131
+ String resolved = UIBundleSocialLoginConfig.resolveStartUrl();
132
+ System.assertNotEquals(null, resolved, 'A valid relative path should resolve to a non-null value');
133
+ System.assert(
134
+ resolved.endsWith('/dashboard'),
135
+ 'A valid relative path must be preserved (optionally prefixed): ' + resolved
136
+ );
137
+ System.assert(!resolved.startsWith('http'), 'Result must not be absolute: ' + resolved);
138
+ }
139
+
140
+ @isTest
141
+ static void getConfigWithMaliciousStartUrlReturnsGracefulEmpty() {
142
+ // End-to-end: a malicious startURL must not crash the endpoint; without
143
+ // a real site context it returns the graceful empty response (unchanged).
144
+ setRequest('https://evil-attacker.com');
145
+
146
+ Test.startTest();
147
+ UIBundleSocialLoginConfig.SocialLoginConfigResponse result =
148
+ UIBundleSocialLoginConfig.getConfig();
149
+ Test.stopTest();
150
+
151
+ System.assertNotEquals(null, result, 'Response should not be null');
152
+ System.assertEquals(0, result.authProviders.size(), 'No providers without site context');
153
+ System.assertEquals(0, result.samlProviders.size(), 'No SAML providers without site context');
154
+ System.assertEquals(null, result.autoRedirectUrl, 'No auto-redirect without providers');
155
+ }
156
+
157
+ // ---- helpers ------------------------------------------------------------
158
+
159
+ /** Asserts a malicious startURL value is not reflected by resolveStartUrl(). */
160
+ private static void assertNotReflected(String maliciousStartUrl) {
161
+ setRequest(maliciousStartUrl);
162
+ String resolved = UIBundleSocialLoginConfig.resolveStartUrl();
163
+ String safe = resolved == null ? '' : resolved;
164
+ System.assert(!safe.contains('evil-attacker'), 'Malicious host must not be reflected: ' + safe);
165
+ System.assert(!safe.startsWith('http'), 'Result must not be an absolute URL: ' + safe);
166
+ System.assert(!safe.startsWith('//'), 'Result must not be protocol-relative: ' + safe);
167
+ System.assert(!safe.contains('\\'), 'Result must not contain a backslash: ' + safe);
168
+ }
169
+
170
+ /** Builds a GET RestContext with the given startURL param (null => param omitted). */
171
+ private static void setRequest(String startUrlValue) {
172
+ RestRequest req = new RestRequest();
173
+ req.requestURI = '/services/apexrest/auth/social-login-config';
174
+ req.httpMethod = 'GET';
175
+ if (startUrlValue != null) {
176
+ req.params.put('startURL', startUrlValue);
177
+ }
178
+ RestContext.request = req;
179
+ RestContext.response = new RestResponse();
180
+ }
101
181
  }
@@ -18,8 +18,8 @@
18
18
  "graphql:schema": "node scripts/get-graphql-schema.mjs"
19
19
  },
20
20
  "dependencies": {
21
- "@salesforce/platform-sdk": "^11.66.2",
22
- "@salesforce/ui-bundle": "^11.66.2",
21
+ "@salesforce/platform-sdk": "^11.66.4",
22
+ "@salesforce/ui-bundle": "^11.66.4",
23
23
  "@tailwindcss/vite": "^4.1.17",
24
24
  "class-variance-authority": "^0.7.1",
25
25
  "clsx": "^2.1.1",
@@ -46,8 +46,8 @@
46
46
  "@graphql-eslint/eslint-plugin": "^4.1.0",
47
47
  "@graphql-tools/utils": "^11.0.0",
48
48
  "@playwright/test": "^1.49.0",
49
- "@salesforce/graphiti": "^11.66.2",
50
- "@salesforce/vite-plugin-ui-bundle": "^11.66.2",
49
+ "@salesforce/graphiti": "^11.66.4",
50
+ "@salesforce/vite-plugin-ui-bundle": "^11.66.4",
51
51
  "@testing-library/jest-dom": "^6.6.3",
52
52
  "@testing-library/react": "^16.1.0",
53
53
  "@testing-library/user-event": "^14.5.2",
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@salesforce/ui-bundle-template-base-sfdx-project",
3
- "version": "11.66.2",
3
+ "version": "11.66.4",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "@salesforce/ui-bundle-template-base-sfdx-project",
9
- "version": "11.66.2",
9
+ "version": "11.66.4",
10
10
  "license": "SEE LICENSE IN LICENSE.txt",
11
11
  "dependencies": {
12
12
  "fast-xml-parser": "^5.9.3",
package/dist/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@salesforce/ui-bundle-template-base-sfdx-project",
3
- "version": "11.66.2",
3
+ "version": "11.66.4",
4
4
  "description": "Base SFDX project template",
5
5
  "license": "SEE LICENSE IN LICENSE.txt",
6
6
  "publishConfig": {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@salesforce/ui-bundle-template-feature-react-authentication",
3
- "version": "11.66.2",
3
+ "version": "11.66.4",
4
4
  "description": "Authentication feature for UI Bundles",
5
5
  "license": "SEE LICENSE IN LICENSE.txt",
6
6
  "author": "",
@@ -16,7 +16,7 @@
16
16
  "clean": "rm -rf dist"
17
17
  },
18
18
  "devDependencies": {
19
- "@salesforce/ui-bundle": "^11.66.2",
19
+ "@salesforce/ui-bundle": "^11.66.4",
20
20
  "@tanstack/react-form": "^1.27.7",
21
21
  "@types/react": "^19.2.7",
22
22
  "@types/react-dom": "^19.2.3",