Report from the security mines | Val Town Blog
Code and prose written on val.townReport from the security mines<br>Tom MacWrightJul 31, 2026<br>One of the best things about working at small companies is the variety. Every<br>few months, your job shifts and you have to pick up a new set of skills. The<br>last couple months I've become the security guy.
I've been the security guy before, but never for a product like Val Town. The<br>core of the product is running untrusted code at scale. The core product is a<br>rich source of security challenges. It's been interesting. Here's what I've<br>learned.
XSS and SQL injection are no longer big categories
When I was starting out in software development, there were a set of<br>tried-and-true vectors.<br>Cross Site Scripting was a big<br>one: injecting some HTML onto someone else's web page and getting a<br>tag to do something nefarious. I don't think it's appreciated the degree to<br>which React and its generation of web frameworks made this<br>a nonissue for most sites. Whatever you slip into your JSX with {} is escaped<br>by default, and the<br>dangerouslySetInnerHTML<br>escape hatch is hard to use accidentally.
SQL injection also loomed large: there's a<br>memorable xkcd cartoon and everything. Back when<br>interacting with a database meant concatenating strings and manually escaping<br>inputs, this was a big deal. But modern tools basically prevent this by design.<br>We use Drizzle, which automatically escapes strings<br>and uses parameters instead of string interpolation elsewhere.
I've learned a lot about Server-Side Request Forgery
Server-Side Request Forgery<br>is when an attacker is able to get your server to make requests to internal or<br>unintended targets. For example, if you're running in a Kubernetes cluster, each<br>server has access to some metadata endpoints. In Render,<br>our servers can do internal networking to the database and a bunch of other<br>destinations. But even though our servers can do this, we don't want user code<br>to do this, and user code runs on our servers. And we expose a lot of ways for<br>people to make requests: Val code can make requests, obviously, and our MCP<br>server supports a web request method.
There are ways to fix this incorrectly, like checking the URL that someone is<br>requesting and making sure it isn't private. If you do that, you'll soon learn<br>about nip.io, a public domain name that maps to private IP<br>addresses. So then you check instead which IP the URL routes to, and make sure<br>that the IP maps to a public server, not a private one. That's good until you<br>discover DNS rebinding, in which<br>that IP can be safe when you check it and then turn nefarious when you make the<br>request. That's called<br>time-of-check to time-of-use<br>(aka TOCTOU) and I'll cover that next! But in the meantime, the real ways to fix<br>this are to either limit network traffic to safe stuff only (a nice option that<br>we don't always have), or to do DNS pinning. In DNS pinning, you figure out what<br>IP the address resolves to, make sure it's safe, and then send the request<br>directly to that IP.
Dependencies are high-noise low-signal
Thanks to<br>Dependabot,<br>Socket, and the big database of dependency CVEs, it's<br>extremely easy to be very aware of all the vulnerabilities that exist in our<br>application. And those alarms matter for our security stance and our upcoming<br>SOC2 audit.
But so far, not a single security report has related to a vulnerability in a<br>dependency. The vast majority of dependency warnings are for transitive or test<br>dependencies that aren't even used in production.
For example, we currently have a version of the brace-expansion NPM module<br>which turns a string like 'file-{a,b,c}.jpg' into 'file-a.jpg', 'file-b.jpg',<br>'file-c.jpg'. It's via the coverage reporter plugin to our test runner, as far<br>away from production traffic as could be. But it's nevertheless a high-severity<br>bug report, and that's how it looks in the dashboards.
So at this point, dealing with vulnerabilities in dependencies requires some<br>context about how they're actually used and whether they'd ever be exploitable.<br>But my favorite way to deal with these vulnerabilities is by removing them. I've<br>been tracking dependency bloat in Val Town<br>since 2024 and<br>chipping away at our numbers, which are<br>the lowest they've ever been.
Adopting Effect has helped in this battle by letting<br>me replace about 8 other dependencies with one big one.<br>e18e.dev's automated tools and recommendations, which are<br>highlighted in npmx package listing pages,<br>have been another tool in this arsenal. Consolidating and removing dependencies<br>helps a lot with potential supply-chain attacks and also makes me happy.
Time-of-check to time-of-use, aka TOCTOU
This is a fun one: the gist is that you'll have some code that does:
const currentCount = await db.count(things);<br>if (currentCount 10) {<br>await db.insert({}).into(things);
The exploit might be obvious here: if you rush in with a lot of requests, then<br>you'll be able to squeak a few by that pass the check but in between checking<br>and adding more,...