Skip to content

Conversation

@AshishKumar4
Copy link
Collaborator

@AshishKumar4 AshishKumar4 commented Nov 6, 2025

Summary

This PR addresses issues with Cloudflare Zero Trust access for preview iframes by routing availability checks through the main application proxy instead of direct subdomain calls.

Changes

  • New API endpoint: Added /api/apps/:id/preview-status (public, HEAD only) that checks preview availability through the proxy
  • Refactored preview resolution: Created worker/utils/previewResolver.ts to centralize preview availability logic (sandbox → dispatcher fallback)
  • Simplified main worker: Refactored worker/index.ts to delegate preview resolution to shared utility
  • Updated frontend: Modified preview-iframe.tsx to use new proxy endpoint instead of direct subdomain HEAD requests

Motivation

Users with Cloudflare Zero Trust policies configured were unable to load preview iframes because the availability check attempted to access user app subdomains directly, which Zero Trust policies blocked. By routing the check through the main application proxy (which users are already authenticated to), the preview availability check bypasses Zero Trust restrictions.

Testing

  • Verify preview availability checks work for apps in sandbox
  • Verify preview availability checks work for deployed (dispatcher) apps
  • Confirm Zero Trust users can now see preview iframes
  • Ensure existing preview functionality remains intact

Related Issues

This may help with preview loading issues mentioned in:


This PR description was automatically generated by Claude Code

@github-actions
Copy link
Contributor

github-actions bot commented Nov 6, 2025

Claude finished @AshishKumar4's task —— View job


Code & Security Review (Critical Path)

Recommendation: REQUEST CHANGES

Summary

This PR successfully addresses the Cloudflare Zero Trust access issue by routing preview availability checks through the main application proxy. The architectural approach is sound and follows good DRY principles by extracting shared logic into previewResolver.ts. However, there are security vulnerabilities that must be addressed before merging.

Security Issues

High Severity

  1. Potential SSRF via Request Forwarding (comment)
    • Location: worker/utils/previewResolver.ts:23
    • The resolvePreview() function forwards arbitrary Request objects to user-controlled sandboxes/dispatchers
    • While previewProxyRoutes.ts creates clean requests, worker/index.ts forwards full original requests with all headers
    • Risk: Sensitive headers (cookies, auth tokens) could leak to user sandboxes
    • Must fix: Add request sanitization or explicit header filtering

Medium Severity

  1. Path Traversal Risk in API Endpoint (comment)

    • Location: worker/api/routes/previewProxyRoutes.ts:15
    • No input validation on appId parameter from URL
    • Should validate format (alphanumeric, hyphens, underscores only) and length (max 63 chars per DNS limits)
  2. Insufficient URL Validation in Frontend (comment)

    • Location: src/routes/chat/components/preview-iframe.tsx:82
    • appId extracted from user-provided URL without validation
    • Could allow injection of special characters or path traversal sequences
  3. Missing Input Validation in Main Worker (comment)

    • Location: worker/index.ts:46
    • appId extracted from hostname without validation before use
    • Should validate against expected patterns (matches sandbox's extractSandboxRoute() approach)

Code Quality Issues

Medium Severity

  1. Inconsistent Error Handling (comment)
    • Location: worker/utils/previewResolver.ts:60
    • Dispatcher errors (403/404/500) silently report as "unavailable" with no error message
    • Makes debugging difficult for operators
    • Should distinguish between "not found" vs "error" states

Positive Aspects

  • Good architectural refactoring - DRY principle applied correctly
  • Properly addresses the Zero Trust access issue
  • Public endpoint correctly configured with AuthConfig.public
  • CORS headers properly configured with Access-Control-Expose-Headers
  • Request deduplication handled via HEAD-only endpoint
  • Follows existing project patterns (route setup, error handling, logging)
  • Type safety maintained throughout (no any types)

Required Changes

Before this PR can be approved:

  1. [CRITICAL] Add request sanitization in resolvePreview() to prevent header leakage
  2. Add input validation for appId in all three locations (API route, frontend, main worker)
  3. Improve error handling in previewResolver.ts to distinguish error types
  4. Consider adding rate limiting to the new public endpoint to prevent abuse

Testing Recommendations

  • Test with malicious appId values: ../../admin, %2e%2e%2fadmin, admin;ls, etc.
  • Verify sensitive headers (Authorization, Cookie) are not forwarded to user sandboxes
  • Test error scenarios: sandbox errors, dispatcher failures, network timeouts
  • Confirm Zero Trust users can access previews with this change

@cloudflare cloudflare deleted a comment from github-actions bot Nov 6, 2025
// Check preview availability for an app (HEAD request only)
app.on('HEAD', '/api/apps/:id/preview-status', setAuthLevel(AuthConfig.public), async (c) => {
const env = c.env;
const appId = c.req.param('id');
Copy link
Contributor

Choose a reason for hiding this comment

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

SECURITY: Path Traversal Risk (Medium)

The appId parameter is extracted directly from the URL without validation. While buildUserWorkerUrl() constructs a subdomain, malicious values could potentially bypass intended behavior.

Recommendation: Add input validation to ensure appId matches expected format:

const appId = c.req.param('id');

// Validate appId format (alphanumeric, hyphens, underscores only)
if (!/^[a-zA-Z0-9_-]+$/.test(appId)) {
    return new Response(null, { status: 400 });
}

// Additional length check to prevent abuse
if (appId.length > 63) { // DNS subdomain limit
    return new Response(null, { status: 400 });
}

* @param env - Worker environment
* @returns PreviewResult with availability status and response
*/
export async function resolvePreview(
Copy link
Contributor

Choose a reason for hiding this comment

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

SECURITY: Potential SSRF via Request Forwarding (High)

The function accepts an arbitrary Request object and forwards it to sandbox/dispatcher without proper sanitization. If the caller doesn't properly sanitize the request, sensitive headers (cookies, auth tokens) could be leaked to user-controlled sandboxes.

Current mitigation: The previewProxyRoutes.ts creates a "clean" request with only the HEAD method, but handleUserAppRequest() in worker/index.ts forwards the full original request.

Recommendation: Ensure this function always sanitizes requests before forwarding:

export async function resolvePreview(
    appId: string,
    request: Request,
    env: Env,
    sanitize: boolean = true // Add flag to enforce sanitization
): Promise<PreviewResult> {
    // Create sanitized request for security-sensitive calls
    const testRequest = sanitize ? new Request(request.url, {
        method: request.method,
        // Only forward safe headers
        headers: new Headers({
            'User-Agent': request.headers.get('User-Agent') || '',
            'Accept': request.headers.get('Accept') || '',
        }),
    }) : request;
    
    const sandboxResponse = await proxyToSandbox(testRequest, env);
    // ...
}

logger.info(`Preview available in dispatcher for: ${appId}`);

return {
available: dispatcherResponse.ok,
Copy link
Contributor

Choose a reason for hiding this comment

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

CODE QUALITY: Inconsistent Error Handling

The function returns available: dispatcherResponse.ok but this could be misleading. A 404 or 500 response would make ok === false, but the error handling below catches exceptions separately.

Issue: If a dispatcher returns 403/404/500, this reports available: false with no error message, making debugging difficult.

Recommendation:

const dispatcherResponse = await worker.fetch(request);

logger.info(`Preview available in dispatcher for: ${appId}, status: ${dispatcherResponse.status}`);

if (!dispatcherResponse.ok) {
    return {
        available: false,
        error: `Dispatcher returned status ${dispatcherResponse.status}`,
    };
}

return {
    available: true,
    type: 'dispatcher',
    response: dispatcherResponse,
};

return null;
}

const statusUrl = `/api/apps/${appId}/preview-status`;
Copy link
Contributor

Choose a reason for hiding this comment

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

SECURITY: Insufficient URL Validation (Medium)

The appId is extracted from a user-provided URL without validation before being used in an API request. A malicious URL could inject path traversal characters or special characters.

Example attack: If url = "https://../../admin.vibesdk.com/", the extracted appId would be ../../admin, potentially causing issues.

Recommendation: Add validation after extraction:

const appId = getAppIdFromUrl(url);
if (!appId || !/^[a-zA-Z0-9_-]+$/.test(appId)) {
    console.log('Invalid app URL or appId:', url);
    return null;
}

async function handleUserAppRequest(request: Request, env: Env): Promise<Response> {
const url = new URL(request.url);
const { hostname } = url;
const appId = hostname.split('.')[0];
Copy link
Contributor

Choose a reason for hiding this comment

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

CODE QUALITY: No Input Validation on appId

The appId is extracted from hostname without validation before being passed to resolvePreview(). This could allow malicious subdomains to trigger unexpected behavior.

Recommendation: Add validation consistent with the sandbox's extractSandboxRoute() pattern:

const appId = hostname.split('.')[0];

// Validate appId (consistent with DNS subdomain limits and security)
if (!appId || appId.length > 63 || !/^[a-zA-Z0-9_-]+$/.test(appId)) {
    logger.warn(`Invalid appId extracted from hostname: ${hostname}`);
    return new Response('Invalid application identifier', { status: 400 });
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant