0

I have a SvelteKit project hosted on Netlify, and the idea is that if a user accesses the route mynetlifyproject.com/route, it should render content from another domain, specifically from app.myrailsproject.com/route.

To make this work, in my Netlify project I created a _redirects file and added the following line:

/route/*                  https://app.myrailsproject.com/route/:splat                                   200

After deploying and testing, the content loads as expected - the HTML and CSS are delivered correctly. However, when inspecting the page, the console shows several errors indicating a failure to fetch the page’s scripts, as shown below:

Access to script at 'https://app.myrailsproject.com/vite/assets/script.af31239f.js' from origin 'https://www.mynetlifyproject.com' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource.

This suggests that the CORS settings on my Rails app are not allowing the files to be delivered to the specified origin. So I went ahead and added the following CORS configuration:

    allow do
      origins 'https://www.mynetlifyproject.com'

      resource '/vite/assets/*',
                headers: :any,
                methods: [:get, :options],
                expose: ['Access-Control-Allow-Origin']
    end
    allow do
      origins '*'
      resource '/public/vite/assets/*', headers: :any, methods: [:post, :get],
      expose: ['Access-Control-Allow-Origin']
    end

Above, I tried two configurations—one specifying the domain directly, and another allowing all origins—but the problem persisted in both cases.

So, I began to suspect that the issue might not be with Rails' CORS configuration, but with NGINX.

I then tried adding the following configuration to NGINX:

cat myapp_production
upstream puma_myapp_production {
  server unix:/var/www/myapp/shared/tmp/sockets/myapp-puma.sock fail_timeout=0;
}

server {
  listen 80;
  server_name localhost myapp.local;
  root /var/www/myapp/current/public;
  try_files $uri/index.html $uri @puma_myapp_production;

  client_max_body_size 4G;
  keepalive_timeout 10;

more_set_headers 'X-Debug-CORS: $http_origin';

  error_page 500 502 504 /500.html;
  error_page 503 @503;

  location @puma_myapp_production {
    proxy_http_version 1.1;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header Host $host;
    proxy_redirect off;
    proxy_set_header Upgrade $http_upgrade;
    proxy_set_header Connection "Upgrade";
proxy_read_timeout 300;
   proxy_connect_timeout 300;
   proxy_send_timeout 300;
      proxy_set_header X-Forwarded-Proto http;
      proxy_pass http://puma_myapp_production;
    # limit_req zone=one;
    access_log /var/www/myapp/shared/log/nginx.access.log;
    error_log /var/www/myapp/shared/log/nginx.error.log;
  }

  location ^~ /assets/ {
    gzip_static on;
    expires max;
    add_header Cache-Control public;
  }

location ~ ^/vite/assets/ {
    add_header 'Access-Control-Allow-Origin' '*' always;
}

location = /vite/assets/indica.f8da5aba.js {
    add_header 'Access-Control-Allow-Origin' '*' always;
    add_header 'Access-Control-Allow-Methods' 'GET, OPTIONS' always;
}

location = /vite/assets/chart.9bbb5b62.js {
    add_header 'Access-Control-Allow-Origin' '*' always;
    add_header 'Access-Control-Allow-Methods' 'GET, OPTIONS' always;
}

  location ^~ /packs/ {
    gzip_static on;
    expires max;
    add_header Cache-Control public;
  }

  location = /50x.html {
    root html;
  }

  location = /404.html {
    root html;
  }

  location @503 {
    error_page 405 = /system/maintenance.html;
    if (-f $document_root/system/maintenance.html) {
      rewrite ^(.*)$ /system/maintenance.html break;
    }
    rewrite ^(.*)$ /503.html break;
  }

  if ($request_method !~ ^(GET|HEAD|PUT|PATCH|POST|DELETE|OPTIONS)$ ){
    return 405;
  }

  if (-f $document_root/system/maintenance.html) {
    return 503;
  }
}

As you can see, there are three attempts to allow CORS: two targeting specific files and one to open access for the whole directory, but the issue still persists.

1 Answer 1

0

I recently had a similar problem with projects hosted on Firebase App Hosting. The problem didn't return explicitly as CORS, instead it threw this error on the client: SyntaxError: Unexpected token 'C', "Cross-site"... is not valid JSON

Looking it up I learned that it was caused by SvelteKit's built-in CSRF protection and the solution was to disable it and build a custom solution using the Handle hook, which I learned in this great article. The three main steps are:

  1. Start by disabling the protection
// svelte.config.ts
import adapter from '@sveltejs/adapter-auto';

export default {
  kit: {
    adapter: adapter(),
    csrf: {
      checkOrigin: false, // Disable built-in origin checking to allow custom middleware
    },
  },
};
  1. Create your custom CSRF handler
// src/hooks/csrf.ts
import type { Handle } from '@sveltejs/kit';
import { json, text } from '@sveltejs/kit';

/**
 * Custom CSRF Protection Middleware
 *
 * @param allowedPaths - List of URL paths that bypass CSRF protection.
 * @param allowedOrigins - Trusted origins allowed to make cross-origin form submissions.
 */
export function csrf(allowedPaths: string[], allowedOrigins: string[] = []): Handle {
    return async ({ event, resolve }) => {
        const { request, url } = event;

        // Get the 'origin' header from the incoming request
        const requestOrigin = request.headers.get('origin');

        // Determine if the request comes from the same origin
        const isSameOrigin = requestOrigin === url.origin;

        // Check if the request origin is explicitly allowed (trusted external origins)
        const isAllowedOrigin = allowedOrigins.includes(requestOrigin ?? '');

        // Define conditions under which the request is forbidden (potential CSRF attack)
        const forbidden =
            isFormContentType(request) && // Checks if the request contains form data
            ['POST', 'PUT', 'PATCH', 'DELETE'].includes(request.method) && // State-changing methods
            !isSameOrigin && // Origin mismatch
            !isAllowedOrigin && // Not explicitly allowed
            !allowedPaths.includes(url.pathname); // Path not explicitly allowed

        // If forbidden, return a 403 Forbidden response immediately
        if (forbidden) {
            const message = `Cross-site ${request.method} form submissions are forbidden`;

            // Return JSON or plain text based on request headers
            if (request.headers.get('accept') === 'application/json') {
                return json({ message }, { status: 403 });
            }
            return text(message, { status: 403 });
        }

        // If the request passes CSRF checks, continue to the next middleware or endpoint
        return resolve(event);
    };

    /**
     * Helper function to check if request 'origin' is allowed.
     */
    function isAllowedOrigin(requestOrigin: string | null, allowedOrigins: string[]) {
        return allowedOrigins.includes(requestOrigin ?? '');
    }

    /**
     * Helper function to determine if request content-type indicates a form submission
     */
    function isFormContentType(request: Request) {
        const type = request.headers.get('content-type')?.split(';', 1)[0].trim().toLowerCase() ?? '';
        return ['application/x-www-form-urlencoded', 'multipart/form-data', 'text/plain'].includes(type);
    }
}
  1. Chain it with your other handles in the server hooks:
// src/hooks.server.ts
import { sequence } from '@sveltejs/kit/hooks';
import { csrf } from './hooks/csrf';

// Define paths exempt from CSRF checks (e.g., public forms or APIs)
const allowedPaths = ['/api/public-form'];

// Define trusted origins allowed to make cross-origin form submissions
const allowedOrigins = ['https://trusted-site.com', 'http://localhost:5173'];

// Export the combined hooks using 'sequence' for better flexibility
export const handle = sequence(
    csrf(allowedPaths, allowedOrigins) // CSRF hook added here
    // You can chain additional middleware hooks here if needed
);

This code is straight from that article, my own implementation changed very little so I think is the best starting point. Definitely check the article for the details, and good luck!

Sign up to request clarification or add additional context in comments.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.