fix: proper CSP handling in basic-host sandboxing (HTTP headers, worker-src, document.write) (#234)
* fix: Move CSP to HTTP headers + add worker-src, frameDomains, baseUriDomains, permissions Security improvements: - CSP is now set via HTTP headers in serve.ts instead of meta tags (meta tag CSP can be tampered with by same-origin content) - CSP passed as query param to sandbox.html for header-based enforcement New CSP/permissions features (borrowed from PR #158): - frameDomains: control frame-src directive for nested iframes - baseUriDomains: control base-uri directive - permissions: camera, microphone, geolocation via iframe allow attribute WebGL fix: - Use document.write() instead of srcdoc for inner iframe content (srcdoc creates opaque origin that breaks WebGL canvas updates) - Add worker-src directive with blob: support (critical for WebGL apps like CesiumJS/Three.js that use workers for tile decoding, terrain processing, image processing) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * refactor: Remove permissions support (defer to PR #158) Keep this PR focused on CSP security fixes only. Permissions (camera, microphone, geolocation) will be handled in #158. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * cleanup McpUiResourceCsp type usage / duplicate defs * fix: guard against CSP injection in domain parameters Validate CSP domain entries to reject characters that could: - Break out of CSP directives (semicolons, newlines) - Inject CSP keywords like 'unsafe-eval' (quotes) - Inject multiple sources in one entry (spaces) This prevents injection attacks where malicious domains could override the security policy. --------- Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
df49e88b2d
commit
8b09f4278c
3 changed files with 572 additions and 155 deletions
42
src/generated/schema.json
generated
42
src/generated/schema.json
generated
|
|
@ -3475,6 +3475,20 @@
|
|||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"frameDomains": {
|
||||
"description": "Origins for nested iframes (frame-src directive).",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"baseUriDomains": {
|
||||
"description": "Allowed base URIs for the document (base-uri directive).",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
|
|
@ -3500,6 +3514,20 @@
|
|||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"frameDomains": {
|
||||
"description": "Origins for nested iframes (frame-src directive).",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"baseUriDomains": {
|
||||
"description": "Allowed base URIs for the document (base-uri directive).",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
|
|
@ -3593,6 +3621,20 @@
|
|||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"frameDomains": {
|
||||
"description": "Origins for nested iframes (frame-src directive).",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"baseUriDomains": {
|
||||
"description": "Allowed base URIs for the document (base-uri directive).",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
|
|
|
|||
28
src/generated/schema.test.ts
generated
28
src/generated/schema.test.ts
generated
|
|
@ -43,8 +43,8 @@ export type McpUiSandboxProxyReadyNotificationSchemaInferredType = z.infer<
|
|||
typeof generated.McpUiSandboxProxyReadyNotificationSchema
|
||||
>;
|
||||
|
||||
export type McpUiSandboxResourceReadyNotificationSchemaInferredType = z.infer<
|
||||
typeof generated.McpUiSandboxResourceReadyNotificationSchema
|
||||
export type McpUiResourceCspSchemaInferredType = z.infer<
|
||||
typeof generated.McpUiResourceCspSchema
|
||||
>;
|
||||
|
||||
export type McpUiSizeChangedNotificationSchemaInferredType = z.infer<
|
||||
|
|
@ -91,10 +91,6 @@ export type McpUiInitializedNotificationSchemaInferredType = z.infer<
|
|||
typeof generated.McpUiInitializedNotificationSchema
|
||||
>;
|
||||
|
||||
export type McpUiResourceCspSchemaInferredType = z.infer<
|
||||
typeof generated.McpUiResourceCspSchema
|
||||
>;
|
||||
|
||||
export type McpUiResourceMetaSchemaInferredType = z.infer<
|
||||
typeof generated.McpUiResourceMetaSchema
|
||||
>;
|
||||
|
|
@ -119,6 +115,10 @@ export type McpUiMessageRequestSchemaInferredType = z.infer<
|
|||
typeof generated.McpUiMessageRequestSchema
|
||||
>;
|
||||
|
||||
export type McpUiSandboxResourceReadyNotificationSchemaInferredType = z.infer<
|
||||
typeof generated.McpUiSandboxResourceReadyNotificationSchema
|
||||
>;
|
||||
|
||||
export type McpUiToolResultNotificationSchemaInferredType = z.infer<
|
||||
typeof generated.McpUiToolResultNotificationSchema
|
||||
>;
|
||||
|
|
@ -171,12 +171,8 @@ expectType<spec.McpUiSandboxProxyReadyNotification>(
|
|||
expectType<McpUiSandboxProxyReadyNotificationSchemaInferredType>(
|
||||
{} as spec.McpUiSandboxProxyReadyNotification,
|
||||
);
|
||||
expectType<spec.McpUiSandboxResourceReadyNotification>(
|
||||
{} as McpUiSandboxResourceReadyNotificationSchemaInferredType,
|
||||
);
|
||||
expectType<McpUiSandboxResourceReadyNotificationSchemaInferredType>(
|
||||
{} as spec.McpUiSandboxResourceReadyNotification,
|
||||
);
|
||||
expectType<spec.McpUiResourceCsp>({} as McpUiResourceCspSchemaInferredType);
|
||||
expectType<McpUiResourceCspSchemaInferredType>({} as spec.McpUiResourceCsp);
|
||||
expectType<spec.McpUiSizeChangedNotification>(
|
||||
{} as McpUiSizeChangedNotificationSchemaInferredType,
|
||||
);
|
||||
|
|
@ -235,8 +231,6 @@ expectType<spec.McpUiInitializedNotification>(
|
|||
expectType<McpUiInitializedNotificationSchemaInferredType>(
|
||||
{} as spec.McpUiInitializedNotification,
|
||||
);
|
||||
expectType<spec.McpUiResourceCsp>({} as McpUiResourceCspSchemaInferredType);
|
||||
expectType<McpUiResourceCspSchemaInferredType>({} as spec.McpUiResourceCsp);
|
||||
expectType<spec.McpUiResourceMeta>({} as McpUiResourceMetaSchemaInferredType);
|
||||
expectType<McpUiResourceMetaSchemaInferredType>({} as spec.McpUiResourceMeta);
|
||||
expectType<spec.McpUiRequestDisplayModeRequest>(
|
||||
|
|
@ -265,6 +259,12 @@ expectType<spec.McpUiMessageRequest>(
|
|||
expectType<McpUiMessageRequestSchemaInferredType>(
|
||||
{} as spec.McpUiMessageRequest,
|
||||
);
|
||||
expectType<spec.McpUiSandboxResourceReadyNotification>(
|
||||
{} as McpUiSandboxResourceReadyNotificationSchemaInferredType,
|
||||
);
|
||||
expectType<McpUiSandboxResourceReadyNotificationSchemaInferredType>(
|
||||
{} as spec.McpUiSandboxResourceReadyNotification,
|
||||
);
|
||||
expectType<spec.McpUiToolResultNotification>(
|
||||
{} as McpUiToolResultNotificationSchemaInferredType,
|
||||
);
|
||||
|
|
|
|||
92
src/generated/schema.ts
generated
92
src/generated/schema.ts
generated
|
|
@ -186,39 +186,29 @@ export const McpUiSandboxProxyReadyNotificationSchema = z.object({
|
|||
});
|
||||
|
||||
/**
|
||||
* @description Notification containing HTML resource for the sandbox proxy to load.
|
||||
* @internal
|
||||
* @see https://github.com/modelcontextprotocol/ext-apps/blob/main/specification/draft/apps.mdx#sandbox-proxy
|
||||
* @description Content Security Policy configuration for UI resources.
|
||||
*/
|
||||
export const McpUiSandboxResourceReadyNotificationSchema = z.object({
|
||||
method: z.literal("ui/notifications/sandbox-resource-ready"),
|
||||
params: z.object({
|
||||
/** @description HTML content to load into the inner iframe. */
|
||||
html: z.string().describe("HTML content to load into the inner iframe."),
|
||||
/** @description Optional override for the inner iframe's sandbox attribute. */
|
||||
sandbox: z
|
||||
.string()
|
||||
.optional()
|
||||
.describe("Optional override for the inner iframe's sandbox attribute."),
|
||||
/** @description CSP configuration from resource metadata. */
|
||||
csp: z
|
||||
.object({
|
||||
/** @description Origins for network requests (fetch/XHR/WebSocket). */
|
||||
connectDomains: z
|
||||
.array(z.string())
|
||||
.optional()
|
||||
.describe("Origins for network requests (fetch/XHR/WebSocket)."),
|
||||
/** @description Origins for static resources (scripts, images, styles, fonts). */
|
||||
resourceDomains: z
|
||||
.array(z.string())
|
||||
.optional()
|
||||
.describe(
|
||||
"Origins for static resources (scripts, images, styles, fonts).",
|
||||
),
|
||||
})
|
||||
.optional()
|
||||
.describe("CSP configuration from resource metadata."),
|
||||
}),
|
||||
export const McpUiResourceCspSchema = z.object({
|
||||
/** @description Origins for network requests (fetch/XHR/WebSocket). */
|
||||
connectDomains: z
|
||||
.array(z.string())
|
||||
.optional()
|
||||
.describe("Origins for network requests (fetch/XHR/WebSocket)."),
|
||||
/** @description Origins for static resources (scripts, images, styles, fonts). */
|
||||
resourceDomains: z
|
||||
.array(z.string())
|
||||
.optional()
|
||||
.describe("Origins for static resources (scripts, images, styles, fonts)."),
|
||||
/** @description Origins for nested iframes (frame-src directive). */
|
||||
frameDomains: z
|
||||
.array(z.string())
|
||||
.optional()
|
||||
.describe("Origins for nested iframes (frame-src directive)."),
|
||||
/** @description Allowed base URIs for the document (base-uri directive). */
|
||||
baseUriDomains: z
|
||||
.array(z.string())
|
||||
.optional()
|
||||
.describe("Allowed base URIs for the document (base-uri directive)."),
|
||||
});
|
||||
|
||||
/**
|
||||
|
|
@ -407,22 +397,6 @@ export const McpUiInitializedNotificationSchema = z.object({
|
|||
params: z.object({}).optional(),
|
||||
});
|
||||
|
||||
/**
|
||||
* @description Content Security Policy configuration for UI resources.
|
||||
*/
|
||||
export const McpUiResourceCspSchema = z.object({
|
||||
/** @description Origins for network requests (fetch/XHR/WebSocket). */
|
||||
connectDomains: z
|
||||
.array(z.string())
|
||||
.optional()
|
||||
.describe("Origins for network requests (fetch/XHR/WebSocket)."),
|
||||
/** @description Origins for static resources (scripts, images, styles, fonts). */
|
||||
resourceDomains: z
|
||||
.array(z.string())
|
||||
.optional()
|
||||
.describe("Origins for static resources (scripts, images, styles, fonts)."),
|
||||
});
|
||||
|
||||
/**
|
||||
* @description UI Resource metadata for security and rendering configuration.
|
||||
*/
|
||||
|
|
@ -521,6 +495,28 @@ export const McpUiMessageRequestSchema = z.object({
|
|||
}),
|
||||
});
|
||||
|
||||
/**
|
||||
* @description Notification containing HTML resource for the sandbox proxy to load.
|
||||
* @internal
|
||||
* @see https://github.com/modelcontextprotocol/ext-apps/blob/main/specification/draft/apps.mdx#sandbox-proxy
|
||||
*/
|
||||
export const McpUiSandboxResourceReadyNotificationSchema = z.object({
|
||||
method: z.literal("ui/notifications/sandbox-resource-ready"),
|
||||
params: z.object({
|
||||
/** @description HTML content to load into the inner iframe. */
|
||||
html: z.string().describe("HTML content to load into the inner iframe."),
|
||||
/** @description Optional override for the inner iframe's sandbox attribute. */
|
||||
sandbox: z
|
||||
.string()
|
||||
.optional()
|
||||
.describe("Optional override for the inner iframe's sandbox attribute."),
|
||||
/** @description CSP configuration from resource metadata. */
|
||||
csp: McpUiResourceCspSchema.optional().describe(
|
||||
"CSP configuration from resource metadata.",
|
||||
),
|
||||
}),
|
||||
});
|
||||
|
||||
/**
|
||||
* @description Notification containing tool execution result (Host -> Guest UI).
|
||||
*/
|
||||
|
|
|
|||
Loading…
Reference in a new issue