```
feat: 增强URL基础地址验证功能 - 在app-instance/frontend/lib/api.ts中实现更严格的URL验证逻辑, 包括检查是否以斜杠开头、包含空格字符,以及使用URL构造函数进行验证 - 在app-instance/frontend/lib/auth-portal.ts中应用相同的URL验证改进, 提升认证门户的基础地址处理安全性 - 在auth-portal/src/lib/auth-client.ts中增强前端跳转URL构建功能, 添加错误处理机制并在URL构造失败时抛出相应异常 - 统一三个文件中的normalizeBaseUrl函数实现,确保一致的输入验证行为 ```
This commit is contained in:
@ -79,7 +79,15 @@ function isBrowser(): boolean {
|
|||||||
function normalizeBaseUrl(value?: string | null): string | null {
|
function normalizeBaseUrl(value?: string | null): string | null {
|
||||||
const trimmed = value?.trim();
|
const trimmed = value?.trim();
|
||||||
if (!trimmed) return null;
|
if (!trimmed) return null;
|
||||||
return trimmed.replace(/\/+$/, '');
|
if (trimmed.startsWith('/') || /\s/.test(trimmed)) return null;
|
||||||
|
const hasScheme = /^[a-z][a-z0-9+.-]*:\/\//i.test(trimmed);
|
||||||
|
const candidate = hasScheme ? trimmed : `http://${trimmed}`;
|
||||||
|
try {
|
||||||
|
const url = new URL(candidate);
|
||||||
|
return url.toString().replace(/\/+$/, '');
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function buildAuthHandoffUrl(response: TokenResponse, nextPath: string): string | null {
|
export function buildAuthHandoffUrl(response: TokenResponse, nextPath: string): string | null {
|
||||||
|
|||||||
@ -6,7 +6,15 @@ const AUTH_PORTAL_PORT = process.env.NEXT_PUBLIC_AUTH_PORTAL_PORT?.trim() || '30
|
|||||||
function normalizeBaseUrl(value?: string | null): string | null {
|
function normalizeBaseUrl(value?: string | null): string | null {
|
||||||
const trimmed = value?.trim();
|
const trimmed = value?.trim();
|
||||||
if (!trimmed) return null;
|
if (!trimmed) return null;
|
||||||
return trimmed.replace(/\/+$/, '');
|
if (trimmed.startsWith('/') || /\s/.test(trimmed)) return null;
|
||||||
|
const hasScheme = /^[a-z][a-z0-9+.-]*:\/\//i.test(trimmed);
|
||||||
|
const candidate = hasScheme ? trimmed : `http://${trimmed}`;
|
||||||
|
try {
|
||||||
|
const url = new URL(candidate);
|
||||||
|
return url.toString().replace(/\/+$/, '');
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function getPortalBaseUrl(): string {
|
function getPortalBaseUrl(): string {
|
||||||
@ -28,4 +36,3 @@ export function buildAuthPortalUrl(path: '/login' | '/register', nextPath?: stri
|
|||||||
}
|
}
|
||||||
return url.toString();
|
return url.toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
51
app-instance/frontend/lib/auth-url.test.ts
Normal file
51
app-instance/frontend/lib/auth-url.test.ts
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
import { afterEach, describe, expect, it, vi } from 'vitest';
|
||||||
|
|
||||||
|
import { buildAuthHandoffUrl } from './api';
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
vi.unstubAllEnvs();
|
||||||
|
vi.resetModules();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('auth URL handling', () => {
|
||||||
|
it('builds auth portal URLs when configured portal host has no scheme', async () => {
|
||||||
|
vi.stubEnv('NEXT_PUBLIC_AUTH_PORTAL_URL', 'auth.example.com');
|
||||||
|
const { buildAuthPortalUrl } = await import('./auth-portal');
|
||||||
|
|
||||||
|
expect(buildAuthPortalUrl('/login', '/mcp')).toBe('http://auth.example.com/login?next=%2Fmcp');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('builds a handoff URL when backend returns a hostname without scheme', () => {
|
||||||
|
const url = buildAuthHandoffUrl({
|
||||||
|
access_token: 'token',
|
||||||
|
refresh_token: '',
|
||||||
|
token_type: 'bearer',
|
||||||
|
user_id: 'u1',
|
||||||
|
username: 'u1',
|
||||||
|
role: 'owner',
|
||||||
|
handoff_code: 'handoff-1',
|
||||||
|
backend_connection: {
|
||||||
|
frontend_base_url: 'workspace.example.com:8088',
|
||||||
|
},
|
||||||
|
}, '/mcp');
|
||||||
|
|
||||||
|
expect(url).toBe('http://workspace.example.com:8088/handoff?code=handoff-1&next=%2Fmcp');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('rejects malformed handoff base URLs instead of throwing URL constructor errors', () => {
|
||||||
|
const url = buildAuthHandoffUrl({
|
||||||
|
access_token: 'token',
|
||||||
|
refresh_token: '',
|
||||||
|
token_type: 'bearer',
|
||||||
|
user_id: 'u1',
|
||||||
|
username: 'u1',
|
||||||
|
role: 'owner',
|
||||||
|
handoff_code: 'handoff-1',
|
||||||
|
backend_connection: {
|
||||||
|
frontend_base_url: 'http://',
|
||||||
|
},
|
||||||
|
}, '/mcp');
|
||||||
|
|
||||||
|
expect(url).toBeNull();
|
||||||
|
});
|
||||||
|
});
|
||||||
@ -19,7 +19,15 @@ export interface ProviderOnboardingPayload {
|
|||||||
function normalizeBaseUrl(value?: string | null): string | null {
|
function normalizeBaseUrl(value?: string | null): string | null {
|
||||||
const trimmed = value?.trim();
|
const trimmed = value?.trim();
|
||||||
if (!trimmed) return null;
|
if (!trimmed) return null;
|
||||||
return trimmed.replace(/\/+$/, '');
|
if (trimmed.startsWith('/') || /\s/.test(trimmed)) return null;
|
||||||
|
const hasScheme = /^[a-z][a-z0-9+.-]*:\/\//i.test(trimmed);
|
||||||
|
const candidate = hasScheme ? trimmed : `http://${trimmed}`;
|
||||||
|
try {
|
||||||
|
const url = new URL(candidate);
|
||||||
|
return url.toString().replace(/\/+$/, '');
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function getFrontendBaseUrl(response: TokenResponse): string | null {
|
function getFrontendBaseUrl(response: TokenResponse): string | null {
|
||||||
@ -110,7 +118,12 @@ export function buildFrontendHandoffUrl(response: TokenResponse, nextPath: strin
|
|||||||
throw new Error(pickPortalText(locale, '后端未返回 handoff code', 'Backend did not return a handoff code'));
|
throw new Error(pickPortalText(locale, '后端未返回 handoff code', 'Backend did not return a handoff code'));
|
||||||
}
|
}
|
||||||
|
|
||||||
const url = new URL('/handoff', frontendBaseUrl);
|
let url: URL;
|
||||||
|
try {
|
||||||
|
url = new URL('/handoff', frontendBaseUrl);
|
||||||
|
} catch {
|
||||||
|
throw new Error(pickPortalText(locale, '目标前端地址格式无效', 'Target frontend URL is invalid'));
|
||||||
|
}
|
||||||
url.searchParams.set('code', handoffCode);
|
url.searchParams.set('code', handoffCode);
|
||||||
if (nextPath) {
|
if (nextPath) {
|
||||||
url.searchParams.set('next', nextPath);
|
url.searchParams.set('next', nextPath);
|
||||||
|
|||||||
Reference in New Issue
Block a user