|
| 1 | +<script> |
| 2 | + import { Figure, ResponsiveImage } from '@maiertech/sveltekit-helpers'; |
| 3 | + import srcProbingBots from './probing-bots.png'; |
| 4 | +</script> |
| 5 | + |
| 6 | +After migrating this website to [Railway](https://railway.com/), I noticed bots probing for |
| 7 | +accidentally exposed vulnerable files: |
| 8 | + |
| 9 | +<Figure caption="HTTP Logs from Railway after first deployment with a custom domain." class="mb-8"> |
| 10 | + <ResponsiveImage src={srcProbingBots} alt="Log entries on Railway.com showing bot requests to |
| 11 | +potentially exposed files, for example, `/.env`." intrinsicWidth={1032} aspectRatio={16/9}></ResponsiveImage> |
| 12 | +</Figure> |
| 13 | + |
| 14 | +Since my website is built with SvelteKit, it returns a 404 for these types of requests. Nothing to |
| 15 | +worry about in terms of security. But all these 404 responses are processed by SvelteKit and consume |
| 16 | +resources on the server. This is especially annoying because Railway's pricing model is based on the |
| 17 | +resources a deployment consumes. |
| 18 | + |
| 19 | +The obvious solution is to host the SvelteKit app behind a web application firewall (WAF) that |
| 20 | +blocks these types of requests before they reach the server. Unfortunately, Railway does not |
| 21 | +currently offer a WAF. So, I thought, why not let SvelteKit play WAF and make it drop these |
| 22 | +requests? |
| 23 | + |
| 24 | +Here is what I came up with: |
| 25 | + |
| 26 | +<Figure caption="hooks.server.ts" class="mb-8"> |
| 27 | + |
| 28 | +```ts |
| 29 | +import { type Handle } from '@sveltejs/kit'; |
| 30 | +import { Blocklist } from '$lib/utils/index.js'; |
| 31 | +import { BLOCKED_PATHS } from '$lib/blocklists/index.js'; |
| 32 | + |
| 33 | +const pathBlocklist = new Blocklist(BLOCKED_PATHS); |
| 34 | + |
| 35 | +export const handle: Handle = async ({ event, resolve }) => { |
| 36 | + const { url } = event; |
| 37 | + |
| 38 | + if (pathBlocklist.isBlocked(url.pathname)) { |
| 39 | + return new Response(null, { status: 204 }); |
| 40 | + } |
| 41 | + |
| 42 | + return resolve(event); |
| 43 | +}; |
| 44 | +``` |
| 45 | + |
| 46 | +</Figure> |
| 47 | + |
| 48 | +Inside the `handle` hook in `hooks.server.ts`, I check if the request path is on a blocklist. The |
| 49 | +blocklist is a |
| 50 | +[`Set`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Set) that |
| 51 | +contains paths used by bots from my Railway logs. Since SvelteKit handles requests at the |
| 52 | +application layer, it always wants to return a response. Even if I return `undefined` after |
| 53 | +detecting a malicious request, SvelteKit still returns a 500 server error. |
| 54 | + |
| 55 | +A 500 server error probably consumes the same amount of resources as the original 404 response. So, |
| 56 | +I don't gain anything with this approach. The 204 no content response in the code above might shave |
| 57 | +off a little bit of processing compared to a 404 or 500 status. But it still returns a response, |
| 58 | +which also messes up my Railway logs because 204 responses show up as successful requests. |
| 59 | + |
| 60 | +Lesson learned: **SvelteKit cannot drop requests at the application layer.** |
| 61 | + |
| 62 | +So, what did I do instead? I proxied the SvelteKit app through |
| 63 | +[Cloudflare](https://www.cloudflare.com/). Its firewall and bot detection take care of malicious |
| 64 | +requests and make sure they never reach the SvelteKit app hosted on Railway. Not exactly an elegant |
| 65 | +solution, but it works. |
| 66 | + |
| 67 | +Cloudflare and [Vercel](https://vercel.com/) have invested a lot into their WAFs lately, and if you |
| 68 | +have ever checked your WAF logs, you might have been stunned by how much garbage they block. I hope |
| 69 | +Railway (and other boutique hosters) will also offer a basic WAF in the not-too-distant future. |
| 70 | +After all, I really want the non-big-tech hosting competition to succeed and be a viable option. |
0 commit comments