Skip to content

PR 6232#6240

Closed
yau-wd wants to merge 3 commits intomainfrom
pr-6232
Closed

PR 6232#6240
yau-wd wants to merge 3 commits intomainfrom
pr-6232

Conversation

@yau-wd
Copy link
Copy Markdown
Contributor

@yau-wd yau-wd commented Apr 17, 2026

#6232 to verify the changes in a real environment.

JunghwanNA and others added 3 commits April 16, 2026 21:24
The clickjacking fix correctly added frame-ancestors controls, but custom allowlists still emitted X-Frame-Options: DENY. This keeps SAMEORIGIN for 'self' and DENY for 'none', while letting explicit allowlists rely on CSP alone so supported embeds continue to work.

Constraint: Legacy X-Frame-Options cannot express arbitrary allowlists

Rejected: Keep DENY for all non-self values | breaks documented IFRAME_ORIGINS allowlists

Confidence: high

Scope-risk: narrow

Reversibility: clean

Directive: Keep CSP as the source of truth for custom iframe allowlists; do not reintroduce contradictory XFO headers

Tested: Added unit coverage for wildcard, self, none, and custom allowlists; local header reproduction for custom origin

Not-tested: Full browser iframe E2E across legacy browsers
…ecomputation

Gemini's review pointed out two low-risk improvements that strengthen the
existing iframe allowlist fix without changing its intent: normalize away empty
CSV tokens before building frame-ancestors, and compute the static header set
once during app startup instead of rebuilding it on every request.

Constraint: IFRAME_ORIGINS is startup configuration, not a dynamic per-request input
Rejected: Larger header-construction refactor | not necessary to address the concrete review feedback
Confidence: high
Scope-risk: narrow
Reversibility: clean
Directive: Keep CSP token normalization aligned with explicit allowlists so empty CSV segments never leak into frame-ancestors
Tested: pnpm --dir packages/server test -- src/utils/XSS.test.ts --runInBand
Not-tested: Full browser iframe embed verification against a running server
Copy link
Copy Markdown
Contributor

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request refactors the iframe security header logic by centralizing it into a new getIframeSecurityHeaders function and improves the parsing of allowed origins by filtering empty strings. Feedback focuses on a security regression where X-Frame-Options: DENY was omitted for custom origins, which could leave legacy browsers unprotected. Additionally, it is recommended to move the header calculation back inside the middleware to ensure robustness against environment changes and to update the test suite to verify the presence of fallback headers.

Comment on lines +120 to +146
export function getIframeSecurityHeaders(): Record<string, string> {
const allowedOrigins = getAllowedIframeOrigins()

if (allowedOrigins === '*') {
return {
'Content-Security-Policy': 'frame-ancestors *'
}
}

if (allowedOrigins === "'self'") {
return {
'Content-Security-Policy': `frame-ancestors ${allowedOrigins}`,
'X-Frame-Options': 'SAMEORIGIN'
}
}

if (allowedOrigins === "'none'") {
return {
'Content-Security-Policy': `frame-ancestors ${allowedOrigins}`,
'X-Frame-Options': 'DENY'
}
}

return {
'Content-Security-Policy': `frame-ancestors ${allowedOrigins}`
}
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

security-high high

The current implementation omits the X-Frame-Options: DENY header when custom origins are provided. This is a security regression for legacy browsers (like IE11) that do not support the Content-Security-Policy: frame-ancestors directive. To maintain the previous security posture, X-Frame-Options: DENY should be included as a fallback for any configuration other than 'self' (which uses SAMEORIGIN) or * (which allows all).

export function getIframeSecurityHeaders(): Record<string, string> {
    const allowedOrigins = getAllowedIframeOrigins()
    const headers: Record<string, string> = {
        'Content-Security-Policy': 'frame-ancestors ' + allowedOrigins
    }

    if (allowedOrigins === "'self'") {
        headers['X-Frame-Options'] = 'SAMEORIGIN'
    } else if (allowedOrigins !== '*') {
        headers['X-Frame-Options'] = 'DENY'
    }

    return headers
}
References
  1. Bypassing user-based authorization is acceptable for API endpoints that are intentionally public (e.g., whitelisted for embedding). This access should not expose sensitive data or allow privilege escalation.

Comment on lines +190 to 196
const iframeSecurityHeaders = getIframeSecurityHeaders()
this.app.use((req, res, next) => {
const allowedOrigins = getAllowedIframeOrigins()
if (allowedOrigins === '*') {
// Explicitly allow all origins (only when user opts in)
res.setHeader('Content-Security-Policy', 'frame-ancestors *')
} else {
const csp = `frame-ancestors ${allowedOrigins}`
res.setHeader('Content-Security-Policy', csp)
// X-Frame-Options for legacy browser support
if (allowedOrigins === "'self'") {
res.setHeader('X-Frame-Options', 'SAMEORIGIN')
} else {
res.setHeader('X-Frame-Options', 'DENY')
}
for (const [headerName, headerValue] of Object.entries(iframeSecurityHeaders)) {
res.setHeader(headerName, headerValue)
}
next()
})
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Moving the call to getIframeSecurityHeaders() outside the middleware means the headers are calculated only once at startup. While this is a minor optimization, it makes the middleware less robust to environment changes (e.g., during testing or in environments where process.env might be updated). For consistency with the previous implementation and other middleware like CORS, it is recommended to call this function inside the middleware.

Suggested change
const iframeSecurityHeaders = getIframeSecurityHeaders()
this.app.use((req, res, next) => {
const allowedOrigins = getAllowedIframeOrigins()
if (allowedOrigins === '*') {
// Explicitly allow all origins (only when user opts in)
res.setHeader('Content-Security-Policy', 'frame-ancestors *')
} else {
const csp = `frame-ancestors ${allowedOrigins}`
res.setHeader('Content-Security-Policy', csp)
// X-Frame-Options for legacy browser support
if (allowedOrigins === "'self'") {
res.setHeader('X-Frame-Options', 'SAMEORIGIN')
} else {
res.setHeader('X-Frame-Options', 'DENY')
}
for (const [headerName, headerValue] of Object.entries(iframeSecurityHeaders)) {
res.setHeader(headerName, headerValue)
}
next()
})
this.app.use((req, res, next) => {
const iframeSecurityHeaders = getIframeSecurityHeaders()
for (const [headerName, headerValue] of Object.entries(iframeSecurityHeaders)) {
res.setHeader(headerName, headerValue)
}
next()
})

Comment on lines +170 to +175
it('omits X-Frame-Options for custom allowlists', () => {
process.env.IFRAME_ORIGINS = 'https://embed.example.com,https://admin.example.com'
expect(getIframeSecurityHeaders()).toEqual({
'Content-Security-Policy': 'frame-ancestors https://embed.example.com https://admin.example.com'
})
})
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Update this test case to reflect the inclusion of the X-Frame-Options: DENY header for custom allowlists, ensuring legacy browser protection is verified.

    it('includes X-Frame-Options: DENY for custom allowlists', () => {
        process.env.IFRAME_ORIGINS = 'https://embed.example.com,https://admin.example.com'
        expect(getIframeSecurityHeaders()).toEqual({
            'Content-Security-Policy': 'frame-ancestors https://embed.example.com https://admin.example.com',
            'X-Frame-Options': 'DENY'
        })
    })

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.

1 participant