Skip to content

Conversation

@sharma-tapas
Copy link
Collaborator

@sharma-tapas sharma-tapas commented Oct 14, 2025

What this PR does / why we need it

Which issue(s) this PR fixes

(optional, in fixes #<issue number>(, fixes #<issue_number>, ...) format, will close the issue(s) when PR gets merged)

fixes #

Special notes for your reviewer

Testing done

please add testing details (logs, screenshots, etc.)

Signed-off-by: Tapas Sharma <tapas@platform9.com>
if (isAuthenticated) {
// User is already authenticated, redirect to dashboard
const returnUrl = new URLSearchParams(window.location.search).get('rd') || '/dashboard/migrations';
window.location.href = returnUrl;

Check warning

Code scanning / CodeQL

Client-side URL redirect Medium

Untrusted URL redirection depends on a
user-provided value
.

Copilot Autofix

AI 2 months ago

The best way to fix this vulnerability is to whitelist return URLs so that only expected, safe paths on your own site can be used as redirect destinations. Specifically, we should check that returnUrl is a pathname within the application's domain (e.g., /dashboard/migrations or any other internal route) and never allow an absolute URL (such as https://evil.com). To do this, we can check if the value of returnUrl starts with a / and does not contain suspicious patterns (e.g., double slashes, schema). If the validation fails, fallback to a safe default route.

You need to adjust both:

  • The redirect in window.location.href = returnUrl; inside checkAuthStatus
  • The use of authService.initiateLogin(returnUrl); in handleLogin (if it also relies on a redirect target: ideally, sanitize the param in both places)

Add a utility function such as getSafeRedirectUrl, and update both usages to use this.

All changes are within ui/src/pages/auth/LoginPage.tsx.


Suggested changeset 1
ui/src/pages/auth/LoginPage.tsx

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/ui/src/pages/auth/LoginPage.tsx b/ui/src/pages/auth/LoginPage.tsx
--- a/ui/src/pages/auth/LoginPage.tsx
+++ b/ui/src/pages/auth/LoginPage.tsx
@@ -28,6 +28,24 @@
   boxShadow: '0 8px 32px rgba(0, 0, 0, 0.1)',
 }));
 
+// Whitelist internal routes for safe redirection
+function getSafeRedirectUrl(redirectUrl: string | null): string {
+  // Only allow relative paths starting with /
+  if (
+    redirectUrl &&
+    redirectUrl.startsWith('/') &&
+    // Prevent // (protocol-like) and suspicious substrings
+    !redirectUrl.startsWith('//') &&
+    !redirectUrl.includes('://') &&
+    !redirectUrl.includes('\r') &&
+    !redirectUrl.includes('\n')
+  ) {
+    return redirectUrl;
+  }
+  // Default safe route
+  return '/dashboard/migrations';
+}
+
 const LoginPage = () => {
   const [isChecking, setIsChecking] = useState(true);
   const [error, setError] = useState<string | null>(null);
@@ -41,7 +59,8 @@
       const isAuthenticated = await authService.checkAuth();
       if (isAuthenticated) {
         // User is already authenticated, redirect to dashboard
-        const returnUrl = new URLSearchParams(window.location.search).get('rd') || '/dashboard/migrations';
+        const unsafeReturnUrl = new URLSearchParams(window.location.search).get('rd');
+        const returnUrl = getSafeRedirectUrl(unsafeReturnUrl);
         window.location.href = returnUrl;
       } else {
         setIsChecking(false);
@@ -54,7 +73,8 @@
 
   const handleLogin = () => {
     try {
-      const returnUrl = new URLSearchParams(window.location.search).get('rd') || '/dashboard/migrations';
+      const unsafeReturnUrl = new URLSearchParams(window.location.search).get('rd');
+      const returnUrl = getSafeRedirectUrl(unsafeReturnUrl);
       authService.initiateLogin(returnUrl);
     } catch (err: any) {
       setError(err.message || 'Failed to initiate login');
EOF
@@ -28,6 +28,24 @@
boxShadow: '0 8px 32px rgba(0, 0, 0, 0.1)',
}));

// Whitelist internal routes for safe redirection
function getSafeRedirectUrl(redirectUrl: string | null): string {
// Only allow relative paths starting with /
if (
redirectUrl &&
redirectUrl.startsWith('/') &&
// Prevent // (protocol-like) and suspicious substrings
!redirectUrl.startsWith('//') &&
!redirectUrl.includes('://') &&
!redirectUrl.includes('\r') &&
!redirectUrl.includes('\n')
) {
return redirectUrl;
}
// Default safe route
return '/dashboard/migrations';
}

const LoginPage = () => {
const [isChecking, setIsChecking] = useState(true);
const [error, setError] = useState<string | null>(null);
@@ -41,7 +59,8 @@
const isAuthenticated = await authService.checkAuth();
if (isAuthenticated) {
// User is already authenticated, redirect to dashboard
const returnUrl = new URLSearchParams(window.location.search).get('rd') || '/dashboard/migrations';
const unsafeReturnUrl = new URLSearchParams(window.location.search).get('rd');
const returnUrl = getSafeRedirectUrl(unsafeReturnUrl);
window.location.href = returnUrl;
} else {
setIsChecking(false);
@@ -54,7 +73,8 @@

const handleLogin = () => {
try {
const returnUrl = new URLSearchParams(window.location.search).get('rd') || '/dashboard/migrations';
const unsafeReturnUrl = new URLSearchParams(window.location.search).get('rd');
const returnUrl = getSafeRedirectUrl(unsafeReturnUrl);
authService.initiateLogin(returnUrl);
} catch (err: any) {
setError(err.message || 'Failed to initiate login');
Copilot is powered by AI and may make mistakes. Always verify output.
if (isAuthenticated) {
// User is already authenticated, redirect to dashboard
const returnUrl = new URLSearchParams(window.location.search).get('rd') || '/dashboard/migrations';
window.location.href = returnUrl;

Check failure

Code scanning / CodeQL

Client-side cross-site scripting High

Cross-site scripting vulnerability due to
user-provided value
.

Copilot Autofix

AI 2 months ago

The best approach is to only allow the rd ("return destination") parameter to navigate to URLs within the application's allowed paths. This avoids both open redirects and XSS vectors. Specifically:

  • Only accept rd values that (a) are relative paths beginning with /, and (b) do not contain protocol specifiers (:), as those could be abused.
  • If rd is not present, or does not match allowed patterns, default to the safe in-app path (/dashboard/migrations).

You'll need to change both usages of returnUrl (in checkAuthStatus and handleLogin). Any line like window.location.href = returnUrl should only occur with a validated, safe path.

Implementation:

  • You can introduce a small utility function, e.g., getSafeReturnUrl, within this file, before the component, to encapsulate the logic for extracting and validating the return URL.
  • That function will parse the query param and only return the path if it is application-local and safe.
  • No new dependencies are needed, as the required operations are simple.

Suggested changeset 1
ui/src/pages/auth/LoginPage.tsx

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/ui/src/pages/auth/LoginPage.tsx b/ui/src/pages/auth/LoginPage.tsx
--- a/ui/src/pages/auth/LoginPage.tsx
+++ b/ui/src/pages/auth/LoginPage.tsx
@@ -12,7 +12,6 @@
 import { styled } from '@mui/material/styles';
 import { authService } from '../../api/auth/authService';
 
-const LoginContainer = styled(Container)(({ theme }) => ({
   minHeight: '100vh',
   display: 'flex',
   alignItems: 'center',
@@ -41,7 +40,7 @@
       const isAuthenticated = await authService.checkAuth();
       if (isAuthenticated) {
         // User is already authenticated, redirect to dashboard
-        const returnUrl = new URLSearchParams(window.location.search).get('rd') || '/dashboard/migrations';
+        const returnUrl = getSafeReturnUrl();
         window.location.href = returnUrl;
       } else {
         setIsChecking(false);
@@ -54,7 +53,7 @@
 
   const handleLogin = () => {
     try {
-      const returnUrl = new URLSearchParams(window.location.search).get('rd') || '/dashboard/migrations';
+      const returnUrl = getSafeReturnUrl();
       authService.initiateLogin(returnUrl);
     } catch (err: any) {
       setError(err.message || 'Failed to initiate login');
EOF
@@ -12,7 +12,6 @@
import { styled } from '@mui/material/styles';
import { authService } from '../../api/auth/authService';

const LoginContainer = styled(Container)(({ theme }) => ({
minHeight: '100vh',
display: 'flex',
alignItems: 'center',
@@ -41,7 +40,7 @@
const isAuthenticated = await authService.checkAuth();
if (isAuthenticated) {
// User is already authenticated, redirect to dashboard
const returnUrl = new URLSearchParams(window.location.search).get('rd') || '/dashboard/migrations';
const returnUrl = getSafeReturnUrl();
window.location.href = returnUrl;
} else {
setIsChecking(false);
@@ -54,7 +53,7 @@

const handleLogin = () => {
try {
const returnUrl = new URLSearchParams(window.location.search).get('rd') || '/dashboard/migrations';
const returnUrl = getSafeReturnUrl();
authService.initiateLogin(returnUrl);
} catch (err: any) {
setError(err.message || 'Failed to initiate login');
Copilot is powered by AI and may make mistakes. Always verify output.
- Removed unused styled Logo component causing TypeScript build error
- Redirect already correctly goes to /dashboard/migrations (not /onboarding)

Signed-off-by: Tapas Sharma <tapas@platform9.com>
password := os.Args[1]

// Generate bcrypt hash using Go's bcrypt library (same as Dex)
fmt.Printf("Generating bcrypt hash for password: %s\n", password)

Check failure

Code scanning / CodeQL

Clear-text logging of sensitive information High

Sensitive data returned by an access to password
flows to a logging call.

Copilot Autofix

AI 2 months ago

The fix is to ensure that sensitive information (the plaintext password) is not logged. The statement at line 26, fmt.Printf("Generating bcrypt hash for password: %s\n", password), should be changed to avoid printing the password. We can simply change it to print a generic message like "Generating bcrypt hash for password...", without including the actual password value. No further changes are necessary, as the rest of the script does not log the password itself. This fix should be made in the file scripts/update-dex-password.go, replacing just line 26. No new imports or methods are required.

Suggested changeset 1
scripts/update-dex-password.go

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/scripts/update-dex-password.go b/scripts/update-dex-password.go
--- a/scripts/update-dex-password.go
+++ b/scripts/update-dex-password.go
@@ -23,7 +23,7 @@
 	password := os.Args[1]
 
 	// Generate bcrypt hash using Go's bcrypt library (same as Dex)
-	fmt.Printf("Generating bcrypt hash for password: %s\n", password)
+	fmt.Println("Generating bcrypt hash for password...")
 	hash, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
 	if err != nil {
 		log.Fatalf("Failed to generate bcrypt hash: %v", err)
EOF
@@ -23,7 +23,7 @@
password := os.Args[1]

// Generate bcrypt hash using Go's bcrypt library (same as Dex)
fmt.Printf("Generating bcrypt hash for password: %s\n", password)
fmt.Println("Generating bcrypt hash for password...")
hash, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
if err != nil {
log.Fatalf("Failed to generate bcrypt hash: %v", err)
Copilot is powered by AI and may make mistakes. Always verify output.
fmt.Printf(" kubectl delete pod -n %s -l app=dex\n", namespace)
fmt.Printf("\nNew credentials:\n")
fmt.Printf(" Email: admin@vjailbreak.local\n")
fmt.Printf(" Password: %s\n", password)

Check failure

Code scanning / CodeQL

Clear-text logging of sensitive information High

Sensitive data returned by an access to password
flows to a logging call.

Copilot Autofix

AI 2 months ago

To fix the clear-text logging of the sensitive password, we should remove or obfuscate the logging statement that prints the plaintext password to the console on line 112. The best practice is to omit printing any sensitive information, or at minimum, to mask (obfuscate) it—e.g., "********"—so the value cannot be retrieved from the logs.

  • Edit scripts/update-dex-password.go: Remove or redact the line that prints the password (fmt.Printf(" Password: %s\n", password)).
  • Optionally, change the message to indicate that the password has been set, but do not display its value.
  • No new methods or imports are needed.

Suggested changeset 1
scripts/update-dex-password.go

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/scripts/update-dex-password.go b/scripts/update-dex-password.go
--- a/scripts/update-dex-password.go
+++ b/scripts/update-dex-password.go
@@ -109,7 +109,7 @@
 	fmt.Printf("  kubectl delete pod -n %s -l app=dex\n", namespace)
 	fmt.Printf("\nNew credentials:\n")
 	fmt.Printf("  Email: admin@vjailbreak.local\n")
-	fmt.Printf("  Password: %s\n", password)
+	fmt.Printf("  Password: [redacted, see input]\n")
 }
 
 func splitLines(s string) []string {
EOF
@@ -109,7 +109,7 @@
fmt.Printf(" kubectl delete pod -n %s -l app=dex\n", namespace)
fmt.Printf("\nNew credentials:\n")
fmt.Printf(" Email: admin@vjailbreak.local\n")
fmt.Printf(" Password: %s\n", password)
fmt.Printf(" Password: [redacted, see input]\n")
}

func splitLines(s string) []string {
Copilot is powered by AI and may make mistakes. Always verify output.
Signed-off-by: Tapas Sharma <tapas@platform9.com>
Signed-off-by: Tapas Sharma <tapas@platform9.com>
Signed-off-by: Tapas Sharma <tapas@platform9.com>
Signed-off-by: Tapas Sharma <tapas@platform9.com>
Signed-off-by: Tapas Sharma <tapas@platform9.com>
Signed-off-by: Tapas Sharma <tapas@platform9.com>
Signed-off-by: Tapas Sharma <tapas@platform9.com>
Signed-off-by: Tapas Sharma <tapas@platform9.com>
Signed-off-by: Tapas Sharma <tapas@platform9.com>
The UI was sending 'Authorization: Bearer null' because it tried to
read tokens from localStorage which was never populated. OAuth2 proxy
handles authentication via cookies (_oauth2_proxy), so we don't need
to manually manage Bearer tokens.

Changes:
- Simplified createAuthenticatedClient() to only set withCredentials
- Removed manual Authorization header that was always null
- Backend nginx/oauth2-proxy forwards auth headers automatically

Fixes: 502 Bad Gateway errors when creating users from UI
Signed-off-by: Tapas Sharma <tapas@platform9.com>
- Added Create, Update, Delete tests for local users
- Tests use healthcheck@vjailbreak.local test user
- Added Body field to APIEndpoint struct for POST/PUT payloads
- Added Skip field to allow disabling individual tests
- Updated documentation with CRUD test details

Total tests increased from 15 to 18 endpoints

Signed-off-by: Tapas Sharma <tapas@platform9.com>
Signed-off-by: Tapas Sharma <tapas@platform9.com>
- Update Dex issuer to https://HOST_IP/dex
- Update OAuth2 Proxy to use HTTPS URLs for external endpoints
- Enable secure cookies (cookie_secure = true)
- Add SSL insecure skip verify for self-signed certs
- Configure ingress for API endpoints (routing to K8s API)
- Update UI ingress paths to avoid catching /api and /apis
- Add priority annotations to ingresses

Working:
- Dex authentication with HTTPS
- OAuth2 Proxy with secure cookies
- UI login redirects to /dashboard
- SSL termination at ingress

Known Issue:
- API endpoints (/api, /apis) return 401 through ingress
- Documented in KNOWN_ISSUES.md for investigation
- Likely requires UI backend to proxy API requests

Signed-off-by: Tapas Sharma <tapas@platform9.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants