<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Code Review on Security Kid Blog</title><link>https://securitykid.com/tags/code-review/</link><description>Recent content in Code Review on Security Kid Blog</description><generator>Hugo -- gohugo.io</generator><language>en-us</language><lastBuildDate>Sun, 18 Oct 2026 00:00:00 +0300</lastBuildDate><atom:link href="https://securitykid.com/tags/code-review/index.xml" rel="self" type="application/rss+xml"/><item><title>I wanted to practice secure code review, but most training uses toy examples So I built SecurityKid</title><link>https://securitykid.com/posts/securitykid-launch/</link><pubDate>Sun, 18 Oct 2026 00:00:00 +0300</pubDate><guid>https://securitykid.com/posts/securitykid-launch/</guid><description>&lt;img src="https://securitykid.com/posts/securitykid-launch/Thumbnail.jpg" alt="Featured image of post I wanted to practice secure code review, but most training uses toy examples So I built SecurityKid" /&gt;&lt;p&gt;I&amp;rsquo;ve been trying to get better at code review for a while now. Tried many online labs, but almost every exercise looks like this:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-py" data-lang="py"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;login&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;query&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;GET&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;q&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nb"&gt;eval&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;A tiny code snippet with a dummy function called &lt;code&gt;login&lt;/code&gt; and an obvious &lt;code&gt;eval&lt;/code&gt;. You find it in ten seconds. You already knew &lt;code&gt;eval&lt;/code&gt; is bad, and you learned nothing about code review that you can transfer to real world tasks.&lt;/p&gt;&#10;&lt;p&gt;That&amp;rsquo;s because real code is not like that. I wanted to see what it actually looks like, so I went to GitHub, where you find real world repositories with vulnerable code, code that people may or may not know is vulnerable (yet).&lt;/p&gt;&#10;&lt;p&gt;After searching through some GHSAs, I learned a lot, especially after wiring DeepSeek into the process as a tutor. None of the security fix PRs looked like the examples above. Some of these files are three thousand lines, some are a hundred, and size is never the point. The bug is three lines somewhere in the middle, and the input goes through a router, a middleware, two helpers, the Strait of Hormuz, and a config default before it reaches the sink. There is no such thing as a &lt;code&gt;vulnerable_function&lt;/code&gt; with the bug completely contained inside it. Real world code is a lot messier than that. The hard part is tracing all the user input correctly, learning how it&amp;rsquo;s handled, and deciding whether that behaviour would cause a security issue or not. Then you have to figure out how to fix it if it is.&lt;/p&gt;&#10;&lt;p&gt;That&amp;rsquo;s exactly the skill I need, and I couldn&amp;rsquo;t find a place to practice it.&lt;/p&gt;&#10;&lt;p&gt;So, based on this journey, I built SecurityKid: a place to learn secure code review with real examples.&lt;/p&gt;&#10;&lt;p&gt;Every case is a real GitHub advisory. I take the vulnerable commit, strip the project&amp;rsquo;s identity, and hand it to you before the fix, with a proper description and some hints if you need them. I also tried to make the brief read like a story. It explains what the code does and how it&amp;rsquo;s used, like what you would get from a real code review task.&lt;/p&gt;&#10;&lt;p&gt;I tried to automate the whole process with LLMs so I can scale it and create tens of high quality challenges without building each one by hand. It still needs human review, but it gets me most of the way there.&lt;/p&gt;&#10;&lt;p&gt;Let me show you a real example from the platform.&lt;/p&gt;&#10;&lt;h2 id="case-name-sorry-wrong-number"&gt;Case name: Sorry, Wrong Number&#10;&lt;/h2&gt;&lt;p&gt;Case info:&#10;TypeScript · 127 lines · 2 files · difficulty 4/5 · high severity&lt;/p&gt;&#10;&lt;p&gt;I picked this one because the vulnerable line looks completely normal.&lt;/p&gt;&#10;&lt;blockquote class="alert alert-warning"&gt;&#10; &lt;div class="alert-header"&gt;&#10; &lt;span class="alert-icon"&gt;⚠️&lt;/span&gt;&#10; &lt;span class="alert-title"&gt;Spoiler&lt;/span&gt;&#10; &lt;/div&gt;&#10; &lt;div class="alert-body"&gt;&#10; &lt;p&gt;I&amp;rsquo;m going to spoil this whole case. If you want to try it cold, skip to &amp;ldquo;Where it is now&amp;rdquo; and come back later.&lt;/p&gt;&#10; &lt;/div&gt;&#10; &lt;/blockquote&gt;&#10;&lt;p&gt;Here&amp;rsquo;s the brief you get:&lt;/p&gt;&#10;&#10; &lt;blockquote&gt;&#10; &lt;p&gt;&lt;strong&gt;Cobalt Works&lt;/strong&gt; runs &lt;strong&gt;Toolhive Mesh&lt;/strong&gt;, a TypeScript MCP server that lets AI agents discover and invoke Actors published by the community. Each Actor gets a standby hostname of the form &lt;code&gt;https://&amp;lt;actorId&amp;gt;.workers.cobaltworks.actor&lt;/code&gt;, and the Mesh hands its client a full endpoint URL built from that host plus an endpoint path the Actor&amp;rsquo;s own definition declares.&lt;/p&gt;&#10;&lt;p&gt;Anyone can publish an Actor, so parts of every Actor&amp;rsquo;s definition are controlled by attackers by design. The Mesh authenticates callers by appending their platform API token to outbound requests toward the chosen endpoint.&lt;/p&gt;&#10;&lt;p&gt;&lt;strong&gt;Your task:&lt;/strong&gt; review the code path that turns (actor ID, declared endpoint path) into the URL handed to the MCP client. Show how a malicious publisher can craft their declared endpoint path so a caller&amp;rsquo;s session is redirected to a host controlled by a third party, and their API token leaked with it.&lt;/p&gt;&#10;&#10; &lt;/blockquote&gt;&#10;&lt;p&gt;Here&amp;rsquo;s the code that builds the URL.&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-ts" data-lang="ts"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kr"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nx"&gt;getActorMCPServerPath&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;actorDefinition&lt;/span&gt;: &lt;span class="kt"&gt;ActorDefinition&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="s1"&gt;&amp;#39;webServerMcpPath&amp;#39;&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="nx"&gt;actorDefinition&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;typeof&lt;/span&gt; &lt;span class="nx"&gt;actorDefinition&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;webServerMcpPath&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;string&amp;#39;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kr"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;webServerMcpPath&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;actorDefinition&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;webServerMcpPath&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;trim&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kr"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;paths&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;parseCommaSeparatedList&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;webServerMcpPath&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;paths&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;paths&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;}&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kr"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;streamablePath&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;paths&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;find&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;path&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;MCP_STREAMABLE_ENDPOINT&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;streamablePath&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;streamablePath&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;}&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;paths&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="p"&gt;}&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kr"&gt;export&lt;/span&gt; &lt;span class="kr"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nx"&gt;getActorMCPServerURL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nx"&gt;actorId&lt;/span&gt;: &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nx"&gt;mcpServerPath&lt;/span&gt;: &lt;span class="kt"&gt;string&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Promise&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kr"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;standbyBaseUrl&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;HOSTNAME&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;mesh.cobaltworks.example&amp;#39;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="o"&gt;?&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;standby.cobaltworks.actor&amp;#39;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;workers.cobaltworks.actor&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="kr"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;standbyUrl&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;getActorStandbyURL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;actorId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;standbyBaseUrl&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sb"&gt;`&lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;standbyUrl&lt;/span&gt;&lt;span class="si"&gt;}${&lt;/span&gt;&lt;span class="nx"&gt;mcpServerPath&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sb"&gt;`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kr"&gt;export&lt;/span&gt; &lt;span class="kr"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nx"&gt;getActorStandbyURL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nx"&gt;actorId&lt;/span&gt;: &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="nx"&gt;standbyBaseUrl&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;workers.cobaltworks.actor&amp;#39;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Promise&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sb"&gt;`https://&lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;actorId&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sb"&gt;.&lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;standbyBaseUrl&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sb"&gt;`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="p"&gt;}&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;That&amp;rsquo;s from one of the files you get when you open that case, &lt;code&gt;src/mcp/actors.ts&lt;/code&gt;. How should you solve it? Read the code.&lt;/p&gt;&#10;&lt;p&gt;Most of it is boring: read a field, trim it, split it on commas, return the first path. There&amp;rsquo;s no shiny &lt;code&gt;eval&lt;/code&gt;, no &lt;code&gt;innerHTML&lt;/code&gt;, no raw query, no random insecure PHP function with undocumented behaviour, just string concatenation.&lt;/p&gt;&#10;&lt;p&gt;Starting with &lt;code&gt;getActorMCPServerPath&lt;/code&gt;, we can see it reads the path an Actor declares after playing with some strings, and &lt;code&gt;getActorMCPServerURL&lt;/code&gt; sticks it on the end of the Actor&amp;rsquo;s standby host. OK, but where does that host come from? &lt;code&gt;getActorStandbyURL&lt;/code&gt; tells us.&lt;/p&gt;&#10;&lt;p&gt;The interesting part is the field name. It&amp;rsquo;s called &lt;code&gt;webServerMcpPath&lt;/code&gt;, the variable is &lt;code&gt;mcpServerPath&lt;/code&gt;, the type is &lt;code&gt;string&lt;/code&gt;, and the tests only use &lt;code&gt;/mcp&lt;/code&gt;, &lt;code&gt;/foo&lt;/code&gt;, and &lt;code&gt;/bar&lt;/code&gt;. Of course I read it as a path, and the code does too, which is exactly the problem, because the URL parser has a different idea of what a path is.&lt;/p&gt;&#10;&lt;p&gt;The standby host is &lt;code&gt;https://&amp;lt;actorId&amp;gt;.workers.cobaltworks.actor&lt;/code&gt;, and the publisher controls &lt;code&gt;webServerMcpPath&lt;/code&gt;. If they set it to &lt;code&gt;@evil.example/mcp&lt;/code&gt;, the final string is:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;https://abc123.workers.cobaltworks.actor@evil.example/mcp&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;blockquote class="alert alert-note"&gt;&#10; &lt;div class="alert-header"&gt;&#10; &lt;span class="alert-icon"&gt;📝&lt;/span&gt;&#10; &lt;span class="alert-title"&gt;Quick URL explainer&lt;/span&gt;&#10; &lt;/div&gt;&#10; &lt;div class="alert-body"&gt;&#10; &lt;p&gt;A URL is not just a string you concatenate. It has a scheme, optional userinfo, a host, a port, a path, a query, and a fragment, and the parser decides which part is which. Put an &lt;code&gt;@&lt;/code&gt; in the wrong place and everything before it becomes userinfo, while everything after it becomes the host. So in the string above, the standby host is not the host anymore, &lt;code&gt;evil.example&lt;/code&gt; is, and that&amp;rsquo;s where the caller&amp;rsquo;s token goes.&lt;/p&gt;&#10; &lt;/div&gt;&#10; &lt;/blockquote&gt;&#10;&lt;p&gt;There&amp;rsquo;s a second variant, &lt;code&gt;.evil.example/mcp&lt;/code&gt;, which gives you &lt;code&gt;https://abc123.workers.cobaltworks.actor.evil.example/mcp&lt;/code&gt;. If the attacker controls &lt;code&gt;evil.example&lt;/code&gt;, they can point that subdomain at their server and get a valid certificate for it, so the &lt;code&gt;@&lt;/code&gt; version is the cleaner demo because it doesn&amp;rsquo;t depend on DNS.&lt;/p&gt;&#10;&lt;p&gt;On its own, a URL that points somewhere else is just an open redirect, easy to ignore. But the Mesh puts the caller&amp;rsquo;s API token on that request, so the redirect takes the token with it. That&amp;rsquo;s how a low severity bug turns into stolen credentials for every caller of the Actor.&lt;/p&gt;&#10;&lt;p&gt;And after reading the code, did you notice how valuable the experience is? You spend time reading code, fighting brain rot and a short attention span, you use your brain&amp;rsquo;s cognitive functions to understand the flow and functionality of the code, then you actually learn something new about how a popular standard works (URL syntax), and you consolidate all of that into one piece of review that correctly highlights how user input could lead to malicious behaviour. Isn&amp;rsquo;t that impressive by itself?&lt;/p&gt;&#10;&lt;h2 id="what-a-finding-looks-like"&gt;What a finding looks like&#10;&lt;/h2&gt;&lt;p&gt;On the platform you&amp;rsquo;d write that up in the findings desk with the file, the lines, the claim, and the reachability path. Something like:&lt;/p&gt;&#10;&#10; &lt;blockquote&gt;&#10; &lt;p&gt;&lt;strong&gt;Location:&lt;/strong&gt; &lt;code&gt;src/mcp/actors.ts&lt;/code&gt;, &lt;code&gt;getActorMCPServerURL&lt;/code&gt;, the line that concatenates the standby URL and the declared path.&lt;/p&gt;&#10;&lt;p&gt;&lt;strong&gt;Root cause:&lt;/strong&gt; the endpoint URL is built by raw string concatenation of a trusted standby origin and a path controlled by the publisher. A value starting with &lt;code&gt;@&lt;/code&gt; is not a path. It turns the standby host into userinfo and makes the text after &lt;code&gt;@&lt;/code&gt; the real host.&lt;/p&gt;&#10;&lt;p&gt;&lt;strong&gt;Exploitability:&lt;/strong&gt; a publisher sets &lt;code&gt;webServerMcpPath&lt;/code&gt; to &lt;code&gt;@evil.example/mcp&lt;/code&gt; on their Actor. Every caller who invokes that Actor gets a URL whose effective host is &lt;code&gt;evil.example&lt;/code&gt;. The Mesh appends the caller&amp;rsquo;s platform API token to that request as a query parameter, so the token goes to the attacker. Replaying it gives the attacker the caller&amp;rsquo;s platform identity: Actor runs, datasets, billing, whatever the token can reach.&lt;/p&gt;&#10;&lt;p&gt;&lt;strong&gt;Fix:&lt;/strong&gt; resolve the declared path against the standby origin with the URL parser, reject anything whose origin differs, and strip userinfo before returning.&lt;/p&gt;&#10;&#10; &lt;/blockquote&gt;&#10;&lt;p&gt;Then you seal it and send it for review. You get a verdict on each part of the claim, with an LLM doing the grading. If you&amp;rsquo;re stuck, there are hints, starting with a nudge and getting more specific as you go.&lt;/p&gt;&#10;&lt;p&gt;Then you see the real fix:&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-diff" data-lang="diff"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="gu"&gt;@@ -23,8 +23,18 @@ export async function getActorMCPServerURL(actorId: string, mcpServerPath: string&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="gd"&gt;- const standbyUrl = await getActorStandbyURL(actorId, standbyBaseUrl);&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="gd"&gt;- return `${standbyUrl}${mcpServerPath}`;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="gi"&gt;+ // Parse the standby URL first so both sides go through the same parser.&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="gi"&gt;+ const standby = new URL(`${await getActorStandbyURL(actorId, standbyBaseUrl)}/`);&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="gi"&gt;+&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="gi"&gt;+ const resolved = new URL(mcpServerPath, standby);&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="gi"&gt;+ if (resolved.origin !== standby.origin) {&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="gi"&gt;+ throw new Error(&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="gi"&gt;+ `Actor ${actorId} declares a webServerMcpPath that resolves outside its standby origin`&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="gi"&gt;+ );&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="gi"&gt;+ }&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="gi"&gt;+&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="gi"&gt;+ // Strip any userinfo that survived parsing. We never want credentials in the URL.&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="gi"&gt;+ resolved.username = &amp;#39;&amp;#39;;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="gi"&gt;+ resolved.password = &amp;#39;&amp;#39;;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="gi"&gt;+ return resolved.toString();&#10;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; }&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The fix parses the standby URL first, resolves the declared path against it, and throws if the origin changes. &lt;code&gt;@evil.example/mcp&lt;/code&gt; becomes a path on the standby host, not a new host, while &lt;code&gt;//evil.example/mcp&lt;/code&gt; and &lt;code&gt;https://evil.example/mcp&lt;/code&gt; resolve to a different origin and get rejected. Then it strips any userinfo that survived. The URL is parsed now, not concatenated.&lt;/p&gt;&#10;&lt;p&gt;The advisory link is there too, with the project name and the GHSA ID you didn&amp;rsquo;t get while reviewing. That&amp;rsquo;s the part I like. You spend twenty minutes with the code, then you find out what it actually was and how it affected a real product.&lt;/p&gt;&#10;&lt;h2 id="so-what-is-this-thing"&gt;So what is this thing?&#10;&lt;/h2&gt;&lt;p&gt;The case above is one of 200+ challenges in the catalog. The catalog covers Go, TypeScript, Python, PHP, Java, and Bash, from under 150 lines to nearly four thousand, and every fix is the actual upstream patch.&lt;/p&gt;&#10;&lt;p&gt;It&amp;rsquo;s not a CTF or a pentesting lab, and it&amp;rsquo;s not a scanner tutorial. There&amp;rsquo;s no flag to capture and no box to pop, and the point isn&amp;rsquo;t to run a tool until it complains. You&amp;rsquo;re reading code the way a reviewer does, and most of the skill is the judgment you need when the tool has nothing to say.&lt;/p&gt;&#10;&lt;h2 id="where-it-is-now"&gt;Where it is now&#10;&lt;/h2&gt;&lt;p&gt;The catalog leans toward the bug classes I could mine and verify first. Some briefs are better than others, and difficulty is still partly a guess.&lt;/p&gt;&#10;&lt;p&gt;I&amp;rsquo;m building this on my own, which is why I&amp;rsquo;m posting now instead of waiting for a polished launch. The first few reviewers will decide what gets built next: more audits of large codebases, more languages, harder cases, better hints, or something I haven&amp;rsquo;t thought of yet.&lt;/p&gt;&#10;&lt;h2 id="closing-the-case"&gt;Closing the case&#10;&lt;/h2&gt;&lt;p&gt;If you&amp;rsquo;ve ever wanted to practice review on code that wasn&amp;rsquo;t written to be reviewed, start with one case. It takes about fifteen minutes. Then tell me where it breaks. Confusing, too easy, too hard, wrong, all of it is useful.&lt;/p&gt;&#10;&lt;p&gt;&lt;strong&gt;&lt;a class="link" href="https://labs.securitykid.com/start" target="_blank" rel="noopener"&#10; &gt;Start a case →&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;&#10;&lt;p&gt;If something is confusing, too easy, too hard, or just wrong, tell me on &lt;a class="link" href="https://www.linkedin.com/company/securitykid" target="_blank" rel="noopener"&#10; &gt;LinkedIn&lt;/a&gt;.&lt;/p&gt;&#10;&lt;p&gt;I read everything.&lt;/p&gt;&#10;</description></item></channel></rss>