mirror of
https://github.com/langgenius/dify.git
synced 2026-09-05 16:55:14 +08:00
Co-authored-by: zxhlyh <jasonapring2015@outlook.com> Co-authored-by: fatelei <fatelei@gmail.com> Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> Co-authored-by: CodingOnStar <hanxujiang@dify.com> Co-authored-by: Cursor <cursoragent@cursor.com> Co-authored-by: 姜涵煦 <hanxujiang@jianghanxudeMacBook-Pro-2.local> Co-authored-by: L1nSn0w <l1nsn0w@qq.com> Co-authored-by: yyh <92089059+lyzno1@users.noreply.github.com>
164 lines
6.2 KiB
TypeScript
164 lines
6.2 KiB
TypeScript
import { afterEach, describe, expect, it, vi } from 'vite-plus/test'
|
|
import { canEmbedPath, proxy } from '@/proxy'
|
|
|
|
const mockEnv = vi.hoisted(() => ({
|
|
NEXT_PUBLIC_ALLOW_EMBED: false,
|
|
NEXT_PUBLIC_CSP_WHITELIST: 'https://example.com',
|
|
NEXT_PUBLIC_MARKETPLACE_URL_PREFIX: '',
|
|
NEXT_PUBLIC_TURNSTILE_SITE_KEY: '',
|
|
}))
|
|
|
|
vi.mock('@/env', () => ({
|
|
env: mockEnv,
|
|
}))
|
|
|
|
const createRequest = (url: string) => {
|
|
const nextUrl = new URL(url) as URL & { clone: () => URL }
|
|
nextUrl.clone = () => new URL(nextUrl)
|
|
|
|
return {
|
|
headers: new Headers(),
|
|
nextUrl,
|
|
} as Parameters<typeof proxy>[0]
|
|
}
|
|
|
|
describe('proxy frame options', () => {
|
|
afterEach(() => {
|
|
mockEnv.NEXT_PUBLIC_ALLOW_EMBED = false
|
|
mockEnv.NEXT_PUBLIC_MARKETPLACE_URL_PREFIX = ''
|
|
mockEnv.NEXT_PUBLIC_TURNSTILE_SITE_KEY = ''
|
|
vi.unstubAllEnvs()
|
|
})
|
|
|
|
it('should allow embedded share routes', () => {
|
|
expect(canEmbedPath('/chatbot/token')).toBe(true)
|
|
expect(canEmbedPath('/workflow/token')).toBe(true)
|
|
expect(canEmbedPath('/completion/token')).toBe(true)
|
|
expect(canEmbedPath('/webapp-signin')).toBe(true)
|
|
expect(canEmbedPath('/agent/token')).toBe(true)
|
|
})
|
|
|
|
it('should deny non-embedded console routes by default', () => {
|
|
expect(canEmbedPath('/chatty')).toBe(false)
|
|
expect(canEmbedPath('/workflowish')).toBe(false)
|
|
expect(canEmbedPath('/completionist')).toBe(false)
|
|
expect(canEmbedPath('/webapp-signing')).toBe(false)
|
|
expect(canEmbedPath('/agents')).toBe(false)
|
|
expect(canEmbedPath('/agent-settings')).toBe(false)
|
|
expect(canEmbedPath('/agentic')).toBe(false)
|
|
expect(canEmbedPath('/agents/agent-1/access')).toBe(false)
|
|
expect(canEmbedPath('/apps')).toBe(false)
|
|
})
|
|
|
|
it('should enforce frame ancestors on protected document routes', () => {
|
|
vi.stubEnv('NODE_ENV', 'production')
|
|
const response = proxy(createRequest('https://cloud.dify.ai/device'))
|
|
const contentSecurityPolicy = response.headers.get('content-security-policy')
|
|
|
|
expect(response.headers.get('x-frame-options')).toBe('DENY')
|
|
expect(contentSecurityPolicy).toContain("script-src 'self'")
|
|
expect(contentSecurityPolicy).toContain("frame-ancestors 'none'")
|
|
})
|
|
|
|
it('should keep published app routes embeddable', () => {
|
|
vi.stubEnv('NODE_ENV', 'production')
|
|
const response = proxy(createRequest('https://udify.app/chat/test-token'))
|
|
const contentSecurityPolicy = response.headers.get('content-security-policy')
|
|
|
|
expect(response.headers.get('x-frame-options')).toBeNull()
|
|
expect(contentSecurityPolicy).toContain("script-src 'self'")
|
|
expect(contentSecurityPolicy).not.toContain('frame-ancestors')
|
|
})
|
|
|
|
it('should allow Cloudflare Turnstile resources when its site key is configured', () => {
|
|
vi.stubEnv('NODE_ENV', 'production')
|
|
mockEnv.NEXT_PUBLIC_TURNSTILE_SITE_KEY = 'site-key-for-tests'
|
|
|
|
const response = proxy(createRequest('https://cloud.dify.ai/signin'))
|
|
|
|
expect(response.headers.get('content-security-policy')).toContain(
|
|
'https://challenges.cloudflare.com',
|
|
)
|
|
})
|
|
|
|
it('should protect device routes when global embedding is enabled', () => {
|
|
mockEnv.NEXT_PUBLIC_ALLOW_EMBED = true
|
|
const response = proxy(createRequest('https://cloud.dify.ai/device/code'))
|
|
|
|
expect(response.headers.get('x-frame-options')).toBe('DENY')
|
|
expect(response.headers.get('content-security-policy')).toContain("frame-ancestors 'none'")
|
|
})
|
|
|
|
it('should deny framing for the Marketplace OAuth authorize route', () => {
|
|
const response = proxy(
|
|
createRequest('https://cloud.dify.ai/account/oauth/authorize?client_id=marketplace-client'),
|
|
)
|
|
|
|
expect(response.headers.get('x-frame-options')).toBe('DENY')
|
|
expect(response.headers.get('content-security-policy')).toContain("frame-ancestors 'none'")
|
|
})
|
|
|
|
it('should allow framing Marketplace pages when a Marketplace origin is configured', () => {
|
|
vi.stubEnv('NODE_ENV', 'production')
|
|
mockEnv.NEXT_PUBLIC_MARKETPLACE_URL_PREFIX = 'https://marketplace.dify.ai'
|
|
|
|
const response = proxy(createRequest('https://cloud.dify.ai/marketplace'))
|
|
|
|
expect(response.headers.get('content-security-policy') ?? '').toMatch(
|
|
/frame-src[^;]*https:\/\/marketplace\.dify\.ai/,
|
|
)
|
|
})
|
|
|
|
it('should not add a Marketplace frame origin when the prefix is unset', () => {
|
|
vi.stubEnv('NODE_ENV', 'production')
|
|
|
|
const response = proxy(createRequest('https://cloud.dify.ai/marketplace'))
|
|
const contentSecurityPolicy = response.headers.get('content-security-policy') ?? ''
|
|
|
|
expect(contentSecurityPolicy).toContain('frame-src')
|
|
expect(contentSecurityPolicy).not.toContain('https://marketplace.dify.ai')
|
|
})
|
|
})
|
|
|
|
describe('proxy CookieYes consent logging', () => {
|
|
it('should allow CookieYes logging requests only through connect-src', () => {
|
|
vi.stubEnv('NODE_ENV', 'production')
|
|
const response = proxy(createRequest('https://cloud.dify.ai/signin'))
|
|
const contentSecurityPolicy = response.headers.get('content-security-policy')!
|
|
const directives = Object.fromEntries(
|
|
contentSecurityPolicy
|
|
.split(';')
|
|
.map((directive) => directive.trim().split(/\s+/))
|
|
.filter(([name]) => name)
|
|
.map(([name, ...sources]) => [name, sources]),
|
|
)
|
|
|
|
expect(directives['connect-src']).toContain('https://log.cookieyes.com')
|
|
for (const directive of ['default-src', 'script-src', 'style-src', 'worker-src', 'media-src'])
|
|
expect(directives[directive]).not.toContain('https://log.cookieyes.com')
|
|
})
|
|
})
|
|
|
|
describe('proxy education entry normalization', () => {
|
|
it('redirects the legacy education action without leaking it into the canonical URL', () => {
|
|
const response = proxy(
|
|
createRequest('https://cloud.dify.ai/?action=getEducationVerify&utm_source=education-site'),
|
|
)
|
|
|
|
expect(response.status).toBe(308)
|
|
expect(response.headers.get('location')).toBe(
|
|
'https://cloud.dify.ai/education/verify?utm_source=education-site',
|
|
)
|
|
})
|
|
|
|
it('does not redirect unrelated actions or paths', () => {
|
|
const unrelatedAction = proxy(createRequest('https://cloud.dify.ai/?action=showSettings'))
|
|
const unrelatedPath = proxy(
|
|
createRequest('https://cloud.dify.ai/apps?action=getEducationVerify'),
|
|
)
|
|
|
|
expect(unrelatedAction.status).toBe(200)
|
|
expect(unrelatedPath.status).toBe(200)
|
|
})
|
|
})
|