{"id":709,"date":"2026-09-21T05:05:01","date_gmt":"2026-09-21T05:05:01","guid":{"rendered":"https:\/\/abrarqasim.com\/blog\/llm-javascript-vulnerability-scanner-the-60-percent-and-the-rigged-zero\/"},"modified":"2026-09-21T05:05:01","modified_gmt":"2026-09-21T05:05:01","slug":"llm-javascript-vulnerability-scanner-the-60-percent-and-the-rigged-zero","status":"publish","type":"post","link":"https:\/\/abrarqasim.com\/blog\/llm-javascript-vulnerability-scanner-the-60-percent-and-the-rigged-zero\/","title":{"rendered":"LLM as a JavaScript Vulnerability Scanner: The 60% and the Rigged Zero"},"content":{"rendered":"<p>Okay, this is going to sound like I&rsquo;m picking a fight with a paper, and I sort of am. A study came out last week claiming that an LLM catches 60% of JavaScript vulnerabilities while the static scanners catch none. That headline made it to my feed three times before lunch. The number is real. The comparison is rigged. And the useful part of the paper is buried under both.<\/p>\n<p>I&rsquo;ve spent the past year putting AI review steps into client pipelines, mostly PHP and Node, so I read the whole thing instead of the abstract. Here&rsquo;s what I think it actually tells you about using an LLM as a javascript vulnerability scanner, and what I&rsquo;d change in a CI pipeline this week because of it.<\/p>\n<h2 id=\"what-the-paper-measured\">What the paper measured<\/h2>\n<p>The paper is <a href=\"https:\/\/arxiv.org\/abs\/2609.13816\" rel=\"nofollow noopener\" target=\"_blank\">Exploring Automated Vulnerability Identification in JavaScript Code Using Large Language Models<\/a> by Kaushik and four co-authors, posted to arXiv on 12 September. They built a dataset of 1,125 real-world JavaScript snippets, each labelled with one of five CWE categories: generic injection, OS command injection, cross-site scripting, SQL injection, and uncontrolled resource consumption. Then they asked three models (Gemini 1.5 Flash, GPT-4o Mini, and a distilled 8B DeepSeek) to classify each snippet, with and without fine-tuning, and compared them to two static analysers.<\/p>\n<p>Out of the box, GPT-4o Mini got 34% right. Gemini got 29%. The DeepSeek 8B model got 8%, which is worse than guessing among five options. After fine-tuning on 1,000 labelled examples, Gemini jumped to 60% and GPT-4o Mini to 49%.<\/p>\n<p>The two SAST tools, <a href=\"https:\/\/github.com\/Bearer\/bearer\" rel=\"nofollow noopener\" target=\"_blank\">Bearer<\/a> and <a href=\"https:\/\/github.com\/ajinabraham\/nodejsscan\" rel=\"nofollow noopener\" target=\"_blank\">NodeJSScan<\/a>, scored 0.0% and 0.8%.<\/p>\n<h2 id=\"why-the-zero-is-meaningless\">Why the zero is meaningless<\/h2>\n<p>Here&rsquo;s the part the headline skips. The snippets are isolated fragments. A static analyser works by tracing data from a source (a request parameter, say) to a sink (a query, a shell call, <code>innerHTML<\/code>). If you hand it a ten-line function with no imports and no call sites, it has no source to trace from, so it reports nothing. That&rsquo;s what the tools are built to do. The authors say so themselves in section 5.1: the setting favours the LLMs, and the near-zero result &ldquo;reflects architectural differences rather than fundamental tool inadequacy.&rdquo;<\/p>\n<p>So the honest comparison is: on a task where SAST can&rsquo;t operate at all, an LLM gets somewhere between a third and 60% right. That&rsquo;s a real finding. &ldquo;LLMs beat SAST&rdquo; is not.<\/p>\n<p>I&rsquo;d go further. I ran Bearer against a Node codebase last year and it caught a real command injection in a file-upload handler that three humans had reviewed. The trace went through two modules. No snippet-level classifier would have seen it, because the vulnerable line looked fine on its own.<\/p>\n<h2 id=\"the-finding-id-actually-act-on\">The finding I&rsquo;d actually act on<\/h2>\n<p>The table nobody quoted is Table 4, which breaks results down by vulnerability type. That&rsquo;s where the paper gets useful.<\/p>\n<p>SQL injection is easy for these models. GPT-4o Mini hit 84% zero-shot, Gemini 82%. OS command injection is similar, 77% to 79%. The authors&rsquo; explanation is that the vulnerable pattern is basically syntactic:<\/p>\n<pre><code class=\"language-js\">\/\/ what the models reliably flag\nconst q = &quot;SELECT * FROM users WHERE id = &quot; + req.params.id;\ndb.query(q);\n\n\/\/ what they flag almost as reliably\nexec(&quot;convert &quot; + req.body.filename + &quot; out.png&quot;);\n<\/code><\/pre>\n<p>String concatenation into a query or a shell command is a shape. It shows up in every security tutorial ever written, so it&rsquo;s all over the pretraining data. The model doesn&rsquo;t need context to recognise it.<\/p>\n<p>Cross-site scripting is a different story. GPT-4o Mini got 46% zero-shot. XSS doesn&rsquo;t have one shape. It&rsquo;s <code>innerHTML<\/code>, it&rsquo;s <code>document.write<\/code>, it&rsquo;s an unescaped attribute, it&rsquo;s a template literal in a server-rendered string. The model has to know which contexts escape and which don&rsquo;t.<\/p>\n<p>Then they tried few-shot prompting: one positive and one negative example, picked by embedding similarity, included in the prompt. GPT-4o Mini&rsquo;s XSS accuracy went from 46% to 76%. Thirty points, no training, just two examples in the context. The same trick did almost nothing for SQL injection, because the model already knew that shape.<\/p>\n<p>That&rsquo;s the sentence I&rsquo;d put on a sticky note. Few-shot helps where the vulnerability is polymorphic and does nothing where it&rsquo;s already a clich\u00e9.<\/p>\n<h2 id=\"chain-of-thought-made-one-model-worse\">Chain of thought made one model worse<\/h2>\n<p>One more result I didn&rsquo;t expect. They tried a chain-of-thought prompt: find the input, decide whether it&rsquo;s exploitable, map it to a CWE. For GPT-4o Mini it added one point of accuracy. For Gemini 1.5 Flash it dropped accuracy from 29% to 18%.<\/p>\n<p>Their guess is that Flash is tuned for short outputs and the reasoning scaffold fights that. I don&rsquo;t know if that&rsquo;s right. What I do know is that I&rsquo;ve been adding &ldquo;think step by step&rdquo; to review prompts by reflex for two years and never once measured whether it helped on the specific model I was calling. That&rsquo;s on me.<\/p>\n<p>The fine-tuning result has the same flavour. Fine-tuning on plain code-to-label pairs got Gemini to 60%. Fine-tuning on code-to-reasoning-to-label pairs got it to 22%, below the untrained baseline. Simpler supervision won.<\/p>\n<h2 id=\"what-id-change-in-a-pipeline-this-week\">What I&rsquo;d change in a pipeline this week<\/h2>\n<p>I&rsquo;m not replacing Semgrep or Bearer with a model. The paper&rsquo;s own conclusion is that the LLM should complement the scanners, and the recall numbers back that up: even the fine-tuned models miss a lot.<\/p>\n<p>What I&rsquo;d do is add an LLM pass that only looks at the diff, and only asks about the categories the paper says it&rsquo;s good at, with examples for the ones it isn&rsquo;t. Something like this in a GitHub Actions step:<\/p>\n<pre><code class=\"language-yaml\">- name: LLM security pass on diff\n  run: |\n    git diff origin\/main...HEAD -- '*.js' '*.ts' &gt; \/tmp\/diff.patch\n    node scripts\/review.mjs \/tmp\/diff.patch\n<\/code><\/pre>\n<p>And the prompt inside <code>review.mjs<\/code> would look roughly like:<\/p>\n<pre><code class=\"language-js\">const prompt = `You are reviewing a JavaScript diff for security issues.\nAnswer only for these categories: SQL injection, OS command injection,\ncross-site scripting.\n\nExample of XSS (vulnerable):\n  el.innerHTML = '&lt;b&gt;' + user.name + '&lt;\/b&gt;';\nExample of XSS (safe):\n  el.textContent = user.name;\n\nFor each hunk, reply with the CWE id and the line, or NONE.\n\n${diff}`;\n<\/code><\/pre>\n<p>Two examples for XSS, none for SQL injection, because that&rsquo;s what the data says. Then Semgrep runs as usual on the whole tree with data flow, and the two reports get posted side by side on the PR. The LLM catches the obvious shapes in new code fast. The scanner catches the cross-module traces the LLM can&rsquo;t see.<\/p>\n<p>I did a version of this for a Laravel client with the agent they were already using, and wrote it up in <a href=\"https:\/\/abrarqasim.com\/blog\/laravel-security-audit-with-the-agent-you-already-use\" rel=\"noopener\">Laravel security audit with the agent you already use<\/a>. The JS version is the same idea with a narrower prompt.<\/p>\n<h2 id=\"where-im-still-unsure\">Where I&rsquo;m still unsure<\/h2>\n<p>The dataset is 125 test snippets, 25 per category. That&rsquo;s small. A 30-point swing on 25 examples is seven or eight snippets changing answer. I&rsquo;d want to see it replicated before I told a client the XSS number was solid.<\/p>\n<p>The models are also a generation old. Gemini 1.5 Flash and GPT-4o Mini were the cheap tier in 2024. I&rsquo;d expect current small models to do better zero-shot on XSS, which would shrink the few-shot gain. I haven&rsquo;t measured that either, and I&rsquo;m suspicious of anyone who says they have without showing the table.<\/p>\n<p>And I keep coming back to the recall problem. A scanner that finds 60% of bugs and is silent on the rest gives you a green check that means less than it looks like it means. The <a href=\"https:\/\/owasp.org\/www-community\/controls\/Static_Code_Analysis\" rel=\"nofollow noopener\" target=\"_blank\">OWASP source code analysis page<\/a> has been making the same point about SAST false negatives for years. Adding a model doesn&rsquo;t fix that. It just adds a second tool with a different blind spot, which is the actual argument for running both.<\/p>\n<p>So here&rsquo;s the thing to do this week. Take your last ten merged JavaScript PRs, run the diff-only prompt above against them with two XSS examples, and see what it flags. Then run Semgrep on the same commits. Put the two lists next to each other. If the LLM found something the scanner didn&rsquo;t, you&rsquo;ve got your justification for the extra CI step. If it found nothing the scanner missed, you&rsquo;ve saved yourself a monthly API bill. Either result is worth an hour. If you&rsquo;d rather I ran that hour for you, I do <a href=\"https:\/\/abrarqasim.com\" rel=\"noopener\">security and pipeline work for clients<\/a>.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>A new paper says LLMs catch 60% of JavaScript vulnerabilities and SAST catches zero. The zero is rigged. The per-CWE table underneath is what I&#8217;d change my CI for.<\/p>\n","protected":false},"author":2,"featured_media":708,"comment_status":"","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"rank_math_title":"","rank_math_description":"A new paper says LLMs catch 60% of JavaScript vulnerabilities and SAST catches zero. The zero is rigged. The per-CWE table underneath is what I'd change my CI for.","rank_math_focus_keyword":"javascript vulnerability scanner","rank_math_canonical_url":"","rank_math_robots":"","footnotes":""},"categories":[4,165,151],"tags":[553,788,44,5,787,154],"class_list":["post-709","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-ai","category-javascript","category-security","tag-ci","tag-code-review-2","tag-javascript","tag-llm","tag-sast","tag-security"],"_links":{"self":[{"href":"https:\/\/abrarqasim.com\/blog\/wp-json\/wp\/v2\/posts\/709","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/abrarqasim.com\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/abrarqasim.com\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/abrarqasim.com\/blog\/wp-json\/wp\/v2\/users\/2"}],"replies":[{"embeddable":true,"href":"https:\/\/abrarqasim.com\/blog\/wp-json\/wp\/v2\/comments?post=709"}],"version-history":[{"count":0,"href":"https:\/\/abrarqasim.com\/blog\/wp-json\/wp\/v2\/posts\/709\/revisions"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/abrarqasim.com\/blog\/wp-json\/wp\/v2\/media\/708"}],"wp:attachment":[{"href":"https:\/\/abrarqasim.com\/blog\/wp-json\/wp\/v2\/media?parent=709"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/abrarqasim.com\/blog\/wp-json\/wp\/v2\/categories?post=709"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/abrarqasim.com\/blog\/wp-json\/wp\/v2\/tags?post=709"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}