Skip to content

Commit d0edab6

Browse files
authored
Fixes CSP directive blocking the iframe issue (#1537)
* fixes CSP directive blocking the iframe issue Signed-off-by: Shoumi <[email protected]> * flake8 fixes Signed-off-by: Shoumi <[email protected]> --------- Signed-off-by: Shoumi <[email protected]>
1 parent cd6d98d commit d0edab6

File tree

4 files changed

+55
-24
lines changed

4 files changed

+55
-24
lines changed

.env.example

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -530,10 +530,15 @@ COOKIE_SAMESITE=lax
530530
# Enable security headers middleware (true/false)
531531
SECURITY_HEADERS_ENABLED=true
532532

533-
# X-Frame-Options setting (DENY, SAMEORIGIN, or ALLOW-FROM uri)
534-
# DENY: Prevents all iframe embedding (recommended for security)
535-
# SAMEORIGIN: Allows embedding from same domain only
536-
# To disable: Set to empty string X_FRAME_OPTIONS=""
533+
# X-Frame-Options setting - Controls iframe embedding (also sets CSP frame-ancestors)
534+
# DENY: Prevents all iframe embedding (recommended for security) → frame-ancestors 'none'
535+
# SAMEORIGIN: Allows embedding from same domain only → frame-ancestors 'self'
536+
# "" (empty string): Allows all iframe embedding → frame-ancestors * file: http: https:
537+
# null or none: Completely removes iframe restrictions (no headers sent)
538+
# ALLOW-FROM uri: Allows specific domain (deprecated, use CSP instead)
539+
#
540+
# Both X-Frame-Options header and CSP frame-ancestors directive are automatically synced.
541+
# Modern browsers prioritize CSP frame-ancestors over X-Frame-Options.
537542
X_FRAME_OPTIONS=DENY
538543

539544
# Other security headers (true/false)

README.md

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1598,7 +1598,7 @@ ContextForge implements **OAuth 2.0 Dynamic Client Registration (RFC 7591)** and
15981598
| `SECURE_COOKIES` | Force secure cookie flags | `true` | bool |
15991599
| `COOKIE_SAMESITE` | Cookie SameSite attribute | `lax` | `strict`/`lax`/`none` |
16001600
| `SECURITY_HEADERS_ENABLED` | Enable security headers middleware | `true` | bool |
1601-
| `X_FRAME_OPTIONS` | X-Frame-Options header value | `DENY` | `DENY`/`SAMEORIGIN` |
1601+
| `X_FRAME_OPTIONS` | X-Frame-Options header value | `DENY` | `DENY`/`SAMEORIGIN`/`""`/`null` |
16021602
| `X_CONTENT_TYPE_OPTIONS_ENABLED` | Enable X-Content-Type-Options: nosniff header | `true` | bool |
16031603
| `X_XSS_PROTECTION_ENABLED` | Enable X-XSS-Protection header | `true` | bool |
16041604
| `X_DOWNLOAD_OPTIONS_ENABLED` | Enable X-Download-Options: noopen header | `true` | bool |
@@ -1617,7 +1617,13 @@ ContextForge implements **OAuth 2.0 Dynamic Client Registration (RFC 7591)** and
16171617
>
16181618
> **Security Validation**: Set `REQUIRE_STRONG_SECRETS=true` to enforce minimum lengths for JWT secrets and passwords at startup. This helps prevent weak credentials in production. Default is `false` for backward compatibility.
16191619
>
1620-
> **iframe Embedding**: By default, `X-Frame-Options: DENY` prevents iframe embedding for security. To allow embedding, set `X_FRAME_OPTIONS=SAMEORIGIN` (same domain) or disable with `X_FRAME_OPTIONS=""`. Also update CSP `frame-ancestors` directive if needed.
1620+
> **iframe Embedding**: The gateway controls iframe embedding through both `X-Frame-Options` header and CSP `frame-ancestors` directive (both are automatically synced). Options:
1621+
> - `X_FRAME_OPTIONS=DENY` (default): Blocks all iframe embedding
1622+
> - `X_FRAME_OPTIONS=SAMEORIGIN`: Allows embedding from same domain only
1623+
> - `X_FRAME_OPTIONS=""`: Allows embedding from all sources (sets `frame-ancestors * file: http: https:`)
1624+
> - `X_FRAME_OPTIONS=null` or `none`: Completely removes iframe restrictions (no headers sent)
1625+
>
1626+
> Modern browsers prioritize CSP `frame-ancestors` over the legacy `X-Frame-Options` header. Both are now kept in sync automatically.
16211627
>
16221628
> **Cookie Security**: Authentication cookies are automatically configured with HttpOnly, Secure (in production), and SameSite attributes for CSRF protection.
16231629
>

mcpgateway/config.py

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -374,7 +374,23 @@ class Settings(BaseSettings):
374374

375375
# Security Headers Configuration
376376
security_headers_enabled: bool = Field(default=True)
377-
x_frame_options: str = Field(default="DENY")
377+
x_frame_options: Optional[str] = Field(default="DENY")
378+
379+
@field_validator("x_frame_options")
380+
@classmethod
381+
def normalize_x_frame_options(cls, v: Optional[str]) -> Optional[str]:
382+
"""Convert string 'null' or 'none' to Python None to disable iframe restrictions.
383+
384+
Args:
385+
v: The x_frame_options value from environment/config
386+
387+
Returns:
388+
None if v is "null" or "none" (case-insensitive), otherwise returns v unchanged
389+
"""
390+
if isinstance(v, str) and v.lower() in ("null", "none"):
391+
return None
392+
return v
393+
378394
x_content_type_options_enabled: bool = Field(default=True)
379395
x_xss_protection_enabled: bool = Field(default=True)
380396
x_download_options_enabled: bool = Field(default=True)

mcpgateway/middleware/security_headers.py

Lines changed: 21 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -273,31 +273,35 @@ async def dispatch(self, request: Request, call_next) -> Response:
273273
# Content Security Policy
274274
# This CSP is designed to work with the Admin UI while providing security
275275
# Dynamically set frame-ancestors based on X_FRAME_OPTIONS setting to stay consistent
276-
x_frame = str(settings.x_frame_options)
277-
x_frame_upper = x_frame.upper()
278-
279-
if x_frame_upper == "DENY":
280-
frame_ancestors = "'none'"
281-
elif x_frame_upper == "SAMEORIGIN":
282-
frame_ancestors = "'self'"
283-
elif x_frame_upper.startswith("ALLOW-FROM"):
284-
allowed_uri = x_frame.split(" ", 1)[1] if " " in x_frame else "'none'"
285-
frame_ancestors = allowed_uri
286-
elif not x_frame: # Empty string means allow all
287-
frame_ancestors = "*"
288-
else:
289-
# Default to none for unknown values (matches DENY default)
290-
frame_ancestors = "'none'"
291-
292276
csp_directives = [
293277
"default-src 'self'",
294278
"script-src 'self' 'unsafe-inline' 'unsafe-eval' https://cdnjs.cloudflare.com https://cdn.tailwindcss.com https://cdn.jsdelivr.net https://unpkg.com",
295279
"style-src 'self' 'unsafe-inline' https://cdnjs.cloudflare.com https://cdn.jsdelivr.net",
296280
"img-src 'self' data: https:",
297281
"font-src 'self' data: https://cdnjs.cloudflare.com",
298282
"connect-src 'self' ws: wss: https:",
299-
f"frame-ancestors {frame_ancestors}",
300283
]
284+
285+
# Only add frame-ancestors if x_frame_options is not None
286+
# When None (or "null"/"none" string), completely disable iframe restrictions
287+
if settings.x_frame_options is not None:
288+
x_frame = str(settings.x_frame_options)
289+
x_frame_upper = x_frame.upper()
290+
291+
if x_frame_upper == "DENY":
292+
frame_ancestors = "'none'"
293+
elif x_frame_upper == "SAMEORIGIN":
294+
frame_ancestors = "'self'"
295+
elif x_frame_upper.startswith("ALLOW-FROM"):
296+
allowed_uri = x_frame.split(" ", 1)[1] if " " in x_frame else "'none'"
297+
frame_ancestors = allowed_uri
298+
elif not x_frame: # Empty string means allow all (including file:// scheme)
299+
frame_ancestors = "* file: http: https:"
300+
else:
301+
# Default to none for unknown values (matches DENY default)
302+
frame_ancestors = "'none'"
303+
304+
csp_directives.append(f"frame-ancestors {frame_ancestors}")
301305
response.headers["Content-Security-Policy"] = "; ".join(csp_directives) + ";"
302306

303307
# HSTS for HTTPS connections (configurable)

0 commit comments

Comments
 (0)