//nefariousplan

Design Debt Driver

A component whose bug-class keeps recurring. Patches address symptoms; the design holds the primitive.

Some components are not unlucky. Some components are fertile.

A normal piece of software accumulates bugs over time, gets them patched, gets audited, and the bug rate tapers. The bugs are incidental: a null dereference here, a logic flaw there, each fix reducing the total. A design-debt-driver does not behave this way. It produces bugs of the SAME CLASS, year after year, decade after decade, with different triggers and different CVEs but the same underlying shape. Each patch closes the specific instance. The instance count keeps going up.

When you see that rhythm, the maintenance team is not the story. The design is.

Mechanism

Every component has a failure profile. Some components fail by arithmetic: a simple bug, one off-by-one, fix it and move on. Some components fail by misuse: the user configured it wrong, the integration was sloppy, the surrounding context set up an unsafe state. Design-debt-drivers fail by class: the component's architecture is a fertile substrate for a specific bug family, and the family reproduces.

The mechanism is compositional. Most real bugs come from the interaction of several design decisions that each, in isolation, looked reasonable. A parser that accepts optional fields. A security gate that fails open for forward compatibility. An administrative interface exposed on the same TCP port as the data interface. A binary format with length fields that the parser trusts. Each of those choices is defensible. Together they produce a pattern where exploit primitives are one new feature away.

The substrate does not fix itself when an instance is patched. The next maintainer adds a new option, or a new caller uses an existing option in a new context, and the same class resurfaces with a different entry point. Defenders track the CVE list. Attackers track the substrate. Attackers win because they are right about where to look.

Exhibits

CVE-2026-32710: MariaDB's "Long Enum Crash" Is Already in mysql.global_priv. JSON_SCHEMA_VALID is the substrate. MDEV-37640 (val_str crash), MDEV-38033 (array-of-objects wrong-result), and now MDEV-38356 (this one, filed as a long-enum crash and assigned CVE-2026-32710 after the OOB write was named) are three consecutive 2025/2026 fixes to a single SELECT-callable validator family. Each instance is a one-line or few-line patch against a specific input shape. The design assumption that the schema validator's helpers receive bounded inputs from upstream is the production line; each new caller and each new value type produces another instance. The patch closes one input shape; the substrate keeps shipping the next.

CVE-2026-42231: xml2js Used defineProperty So __proto__ Wouldn't Mutate. The Property It Wrote Is The Pollution.. xml2js's assignOrPush writes XML tag names through Object.defineProperty specifically to bypass the __proto__ setter at parse time. The choice closed an earlier parse-time prototype-pollution CVE in the library. It also produces a parsed object with an own enumerable __proto__ data property whose value is the attacker's XML payload. That smuggled key survives JSON.stringify, survives JSON.parse, and re-pollutes the prototype chain via the [[Set]] accessor on the next Object.assign. xml2js issue #721 (March 2026) is still open. n8n's v1.123.32 patch adds tagNameProcessors / attrNameProcessors to rename three names at the consumer's parser config; the library's primitive ships unchanged. Every other xml2js consumer that does not install the same denylist produces the same smuggled object.

CVE-2025-1094: PQescapeLiteral Asks How Long. It Does Not Ask Whether.. CVE-2025-1094 is design-debt-driver in its small-substrate form. The substrate is not a missing dependency but a missing call: pg_encoding_verifymbchar has lived in src/common/wchar.c since the early 2010s, in the same translation unit as the pg_encoding_mblen the escape family was built on, and the four PQescape* functions plus fmtId plus appendStringLiteral never called it. The 17.3 / 16.7 / 15.11 / 14.16 / 13.19 patch by Andres Freund wired the verifier into six call sites at once, the way Camel's CVE-2026-40453 fix wired setLowerCase(true) into five subclasses at once. The substrate that produces the next instance is the same shape both posts describe: the function name promises a security property whose enforcement depends on a per-callsite developer choice to call a validator that lives one file away. The next escape function someone writes against pg_encoding_mblen in a binding library, a new front-end tool, or a re-implementation for performance, will reproduce the same primitive.

CVE-2026-40466: HTTP Discovery Was Row Two Of A Fourteen-Row Deny List. CVE-2026-40466 is the second row of the BrokerView deny list to receive a CVE number. CVE-2026-34197 was the first. Twelve more rows have been added to the same Set.of(...) literal in the same file by the same author across the same five-week window, in pull requests whose titles end in "part 2." The deny list grows because the parameter type is String and the URI registry is open: every scheme TransportFactory.findTransportFactory() can resolve is a row that does not yet exist on the boundary. The patches address rows; the parameter type holds the primitive.

CVE-2026-5865: Maglev's BuildCheckSmi Took a Parameter Named `elidable` and Did Not Read It. V8's optimising compilers are a canonical Design Debt Driver: "static type analysis claimed Smi, the runtime value was not, the lowering pass elided the check on the typer's word" has been a recurring bug class in TurboFan for the better part of a decade, with consistent fix shape (tighten the elision condition, or pass the caller a flag they can use to demand the check). CVE-2026-5865 is that class reproducing in Maglev, V8's mid-tier compiler that shipped in 2023. The exhibit's particular value is the fix shape itself: BuildCheckSmi already had the parameter named elidable, evidence the design recognised the pattern. Pre-patch, the function's body did not reference it. The fix is && elidable, the missing operand the parameter had been carrying for callers to no effect. The component changed (TurboFan to Maglev). The lesson did not transfer. Each fix is real; the architecture that produces the instances is the same.

CVE-2025-22457: Ivanti's End-of-Life Branch Was UNC5221's Lab. CVE-2025-22457 is the next exhibit in Ivanti's web-process design-debt lineage. The substrate is the web process that runs as the request dispatcher on ICS, IPS, ZTA, and Pulse Connect Secure across a shared codebase, and parses untrusted HTTP and IKEv2 inputs into fixed-size stack buffers. CVE-2024-21887 (command injection through the web layer), CVE-2025-0282 (IKEv2 strncpy overflow), and CVE-2025-22457 (dispatchRequest strlcpy overflow) are three CVEs, three functions, three protocols, one substrate. The 22.7R2.6 patch rewrote WebRequest::dispatchRequest and turned on stack canaries, ASLR, NX, and FORTIFY_SOURCE that the prior build had been compiled without; the substrate (a request dispatcher whose bounds discipline depends on per-callsite developer attention, shipped without compile-time mitigations across multiple branches including end-of-life ones) is unchanged. UNC5221's six-month cadence across the same process is the substrate's natural metabolism.

CVE-2026-34197: ActiveMQ's 5.19.4 Patch Denied One Scheme. Six Weeks Later, the Same Line Denies Fourteen.. CVE-2026-34197 is design-debt-driver in deny-list form. The component is BrokerView, a JMX bean whose two addConnector(String) and addNetworkConnector(String) methods accept caller-supplied Discovery URI strings and route them through ActiveMQ's transport factory registry. Each registered scheme is a META-INF/services/org/apache/activemq/transport/ entry that the URI parser hands the rest of the string to with config-fetching authority. The 5.19.4 patch added a four-line validateAllowedScheme method whose body was if (scheme.equals("vm")) throw. Five commits and six weeks later the same line in the same file is Set.of("vm", "http", "multicast", "zeroconf", "discovery", "fanout", "mock", "peer", "failover", "proxy", "reliable", "simple", "udp", "masterslave"), and the most recent commit title is "part 2." The substrate (a public MBean method that takes a string and chooses an internal subsystem) is unchanged. Each new entry is a CVE the previous patch did not close.

CVE-2026-26332: vm2 Wraps Every Error the Sandbox Throws. SuppressedError Carries the Two V8 Threw.. Fourth vm2 exhibit. The C++-construction class. CVE-2026-24118 (Array species via C++ read) and CVE-2026-26956 (wasm JSTag via C++ slot) are read-side bypasses; CVE-2026-26332 is a construction-side bypass where V8 manufactures host-realm Error objects on the sandbox's behalf during ES2024 disposal-protocol rethrow. The PoC chains two language features (DisposableStack and SuppressedError) and reads as a tutorial example: two defer registrations, one dispose, one catch, three property accesses to the host Function. The defense will be reactive re-wrapping on the disposal protocol's rethrow path because the sandbox cannot prevent V8 from constructing the carriers in the first place. The vocabulary of named per-feature defenses (resetPromiseSpecies, neutralizeArraySpeciesBatch, the forthcoming SuppressedError wrap) grows by one entry per ECMAScript edition that ships an error-carrying carrier. The substrate, a Proxy bridge over a runtime that constructs objects on its own initiative, is unchanged.

CVE-2026-26956: vm2's Transformer Rewrites Every catch It Parses. WebAssembly's try_table Is Bytecode.. Third vm2 exhibit. The transformer-blindspot variant. vm2's hardening contract is "every dangerous operation has a syntax form, and the AST walker rewrites it before execution." The contract is exhaustive over the AST and silent over everything else. WebAssembly Exception Handling shipped a catch instruction (try_table) that lives in bytecode and a JavaScript handle (WebAssembly.JSTag) that is read by the wasm runtime through a C++ slot. Neither the transformer nor the Proxy bridge has a handle on either. The fix is to delete WebAssembly.JSTag from the sandbox global, the same shape of fix that closed the resetPromiseSpecies family. The driver is the design. A new V8 feature whose execution path runs outside the parser produces a new attack category. The deletion list grows.

CVE-2026-0073: adbd's EVP_PKEY_cmp Was the 2020 OpenIKED Bug. EVP_PKEY_cmp is the substrate. OpenIKED shipped CVE-2020-16088 by treating its return value as a boolean in 2020. Android's adbd shipped CVE-2026-0073 by treating its return value as a boolean in 2026. The function returns 1 on match, 0 on mismatch, -1 on type difference, -2 on unsupported. OpenSSL 3.0 deprecated it in September 2021 and added EVP_PKEY_eq precisely because the boolean shape kept producing this exact bug. BoringSSL kept the old function. AOSP's fix did not migrate to a safer API; it added one == 1 check at the call site. The debt is the API. Every consumer who reads the function name and assumes boolean semantics writes the same auth bypass, and the pattern keeps coming back until the substrate is replaced rather than patched.

CVE-2026-34975: Plunk's CRLF Patch Lives in the Schema. The Schema Missed Two of Eight Fields.. Plunk's sendRawEmail builds the SMTP message by string-templating eight caller-controlled values into a multiline RFC 5322 header block. The substrate — template-literal MIME construction with no encoding pass at the boundary — guarantees CRLF injection any time a new field is added without a matching guard. Safety lives one layer up, in the Zod send action schema, where regex like /^[^\r\n"]+$/ is bolted onto fields one at a time. The first patch (Feb 2026) closed attachment.contentId. The second patch (Mar 2026, generic title, no CVE attribution) closed subject, from.name, headers keys, headers values, and attachment.filename — the four vectors the published PoC happened to test. Two interpolations the PoC didn't try — to.name and attachment.contentType — still flow into the same template unguarded in the version the CVE was filed against. A third commit landing the day the CVE published patched a different bug in the same function (a stray blank line collapsing the header section per RFC 5322 §2.1) without touching the construction model. The pattern: every field added to the schema is a future field-by-field CVE, because the construction layer never learned to encode.

CVE-2026-33017: Langflow's flow_id Authorized. The data Parameter Ran.. Langflow has now produced two unauthenticated exec() CVEs in thirteen months, both reaching the same component-instantiation pipeline through different doors. CVE-2025-3248 was POST /api/v1/validate/code, an unauthenticated endpoint patched by adding authentication; the Flodrix botnet weaponized it before most operators had finished patching. CVE-2026-33017 is POST /api/v1/build_public_tmp/{flow_id}/flow, an unauthenticated endpoint that is unauthenticated by name, and whose 1.9.0 patch leaves the offending data parameter in the FastAPI signature and pushes the safety invariant one argument further down the call stack. The pattern's mechanism_md says "the substrate does not fix itself when an instance is patched"; Langflow's substrate is "users author Python components and the server exec()s them," and every endpoint that touches a flow is an entry into the same primitive. Each fix closes the call site that produced its CVE; the next CVE is the next door the next researcher reaches.

CVE-2026-32202: The Trust Check Runs at the Click. The Coercion Runs at the Icon.. CVE-2026-32202 is the design-debt-driver shape with a true-crime cadence. Same primitive, same operator (APT28), same file in shell32.dll, same enclosing CPL applet IDList structure, with the bypass landing within weeks of the patch. Microsoft's April fix wired SmartScreen verification through ControlPanelLinkSite into ShellExecuteExW. The May exploit reaches the same PathFileExistsW SMB coercion via CControlPanelFolder::GetUIObjectOf during ordinary folder enumeration, an entry point the patch did not touch. Each Shell side effect (icon extraction, tooltip resolution, preview rendering, execute) is its own potential entry point against the same primitive; the patch closed one. The substrate, that shell32 composes attacker-influenced UNC paths through multiple Shell pipelines and only one of them has a trust verifier, holds. CVE-2017-8464, CVE-2024-43461, CVE-2026-21510, CVE-2026-32202: same bug class, same vendor, four CVEs, and the patch architecture is still the same shape.

CVE-2026-21636: --allow-net Permitted UDS Because UDS Is Not Net. CVE-2026-21636 is design-debt-driver in the network-permission space. The Node permission model gates network at each C++ wrap-class call site: TCPWrap::Connect got ERR_ACCESS_DENIED_IF_INSUFFICIENT_PERMISSIONS(env, kNet, ...) when the network surface stabilized; PipeWrap::Connect did not. The fix in v25.3.0 adds the same macro to the missing call site. The substrate (a permission model whose enforcement is per-handle-class convention rather than a libuv-level or syscall-level boundary) is unchanged. The next handle class Node ships, QUIC, HTTP/3, anything new with its own ConnectionWrap subclass, is its own opportunity for the same one-line omission. PipeWrap::Bind in the patched release still has no permission check at all.

CVE-2017-8046: PathToSpEL Did What Its Name Said. CVE-2017-8046 is the substrate moment in Spring's SpEL-injection lineage. The class PathToSpEL.java translated JSON Patch path fields into SpEL expressions by splitting on / and joining the rest into Spring's property-access language; inputs containing no / fell through unmodified to SpelExpressionParser.parseExpression, which evaluated T(java.lang.Runtime).getRuntime().exec(...) chains as a matter of routine. The 2017 fix landed in three commits across two months from the same engineer, and only the third commit named the substrate by deleting the class. The same architectural shape, caller-influenced strings reaching a reflection-capable expression engine, then shipped CVE-2018-1273 in Spring Data Commons, CVE-2022-22963 in Spring Cloud Function, and CVE-2022-22965 (Spring4Shell) across the next five years. Four CVEs, four products, one substrate. Each patch closed its instance. The next CVE was the next entry point Spring exposed.

CVE-2026-40453: Camel Patched One Header Filter In 2025. Five More Needed The Same Line.. CVE-2026-40453 is design-debt-driver in its purest form. Apache Camel ships six classes that extend DefaultHeaderFilterStrategy to filter inbound headers, and the parent's lowerCase field defaults to false. The 2025 patch for CVE-2025-27636 was a one-line opt-in (setLowerCase(true)) added to one subclass; thirteen months later the maintainer added the byte-for-byte identical line to the other five. The substrate, the parent class default, was never changed, so every future subclass that extends it inherits the same one-line CVE waiting to be filed.

CVE-2025-8061: LnvMSRIO Does What Its Name Says. Lenovo's LnvMSRIO.sys is the OEM-utility expression of the pattern. The driver is named after the capability it exposes: Model-Specific Register I/O plus arbitrary physical memory R/W, all behind four IOCTLs reachable from a CreateFileW call by a standard user. CVE-2025-8061 is the access-control instance. The substrate (an OEM ships a signed kernel driver whose declared job is exposing kernel-mode primitives to user-mode callers, then catches up to access control several major versions later) is the design. Each new OEM utility driver enters the LOLDrivers catalog on the same cadence; the next CVE is not the same code, it is the same shape in a different vendor's binary.

CVE-2026-33937: Handlebars Trusts Its Own AST. Handlebars is a design-debt-driver in its purest form: a public API surface (compile, partials, decorators, the CLI precompiler, runtime helpers) that converts caller-influenced inputs into JavaScript the runtime executes, with every surface trusting upstream invariants no caller is contractually obliged to maintain. Prior instances span seven years (CVE-2019-19919 lookupProperty pollution, CVE-2019-20920 Object.__defineGetter__ RCE, CVE-2021-23369 compileToCallback strict-mode bypass, CVE-2021-23383 __proto__ pollution in compile). The March 2026 family lands in a single commit that closes eight advisories at once, three of which share the literal phrase "JavaScript Injection via AST Type Confusion." The 67-line patch validates three node fields. The substrate (compile-accepts-string-or-AST, partials accept objects, runtime helpers compose with library internals) is unchanged. The next family is the next researcher finding the next surface.

CVE-2026-42945: The Other Half of the 2012 Patch. NGINX's script engine is a clean exhibit of the pattern. Two parallel flags, is_args and quote, govern the same escape-budget predicate in ngx_http_script_copy_capture_len_code and ngx_http_script_copy_capture_code. The two flags have asymmetric reset policies: quote has been zeroed at the end of ngx_http_script_regex_end_code since release 0.1.29 in 2005, and is_args was not. The asymmetry produced the same buffer-overrun twice. In 2012, ticket #162 closed an is_args leak from a sub-engine. In 2026, CVE-2026-42945 closes the leak from the main engine. Both fixes are one line. Both reuse the same opening clause in their commit messages. Fourteen years separate them. The driver is the predicate's fan-in over flags whose reset hooks do not all live in the same block.

CVE-2026-34159: The Deserializer Three CVEs Have Not Patched. llama.cpp's RPC backend has produced three CVEs whose root the GHSA author groups under one sentence: deserialize_tensor() does not validate tensor->data when buffer=0. CVE-2024-42478 reached the deserializer via GET_TENSOR; the 2024 fix added bounds-checking in the get_tensor handler. CVE-2024-42479 reached it via SET_TENSOR; the 2024 fix added bounds-checking in the set_tensor handler. CVE-2026-34159 reached it via GRAPH_COMPUTE; the 2026 fix added a null-buffer check in create_node. Three patches in three different functions, each closing the call site that produced its CVE. The deserializer that returns the unsafe tensor is the same in 2026 as it was before any of the three patches landed. The next RPC command that takes a tensor on the wire and uses the result's data field is its own opportunity for the same primitive to surface as a fourth CVE.

CVE-2026-23918: m->spurge Was an h2_ihash. The Array That Replaced It Kept the Assertion, Not the Dedup.. mod_http2's h2_mplx is the substrate. Streams are tracked across three containers (m->streams, m->shold, m->spurge) with hand-coded transitions across at least four code paths, and the invariants between containers are enforced as ap_assert panics at consumers rather than as constraints at producers. CVE-2023-44487 (Rapid Reset), CVE-2023-45802 (the Apache-specific Rapid Reset memory-release variant), and CVE-2026-23918 are three stream-lifecycle CVEs in twenty-six months from the same machinery. The 2.4.67 patch enforces one invariant at one producer (the add_for_purge dedup) and leaves the assertion that named the invariant in 2021 in place at the consumer. Each patch closes the instance that produced its CVE. The substrate that produces the next one — many code paths transitioning the same h2_stream pointer between three containers under invariants enforced as panics — is unchanged.

CVE-2023-50094: The CVE Number Is a Drawer, and reNgine Put Seven Bugs In It. reNgine is a design-debt-driver in its clearest form. Seven command injection fixes in three years across seven different files, each following the same shape: user-controlled string concatenated into a shell command, passed to subprocess with shell=True or the split-and-exec equivalent. The central run_command utility accepts a raw string and a shell=True flag; seventeen call sites in tasks.py pass that flag today. Each patch closes its one call site. The substrate that makes the next call site exploitable ships unchanged. The CVE count under-counts the class because six of the seven patches never got a CVE number; the only one that did, CVE-2023-50094, has absorbed unrelated bugs from other reporters, so the ID no longer points at a single fault. Defenders subscribe to the bug class by running reNgine.

CVE-2026-39987: The Only WebSocket in Marimo Without an Auth Check Was the One That Forks a Shell. Starlette plus WebSockets is the substrate. Starlette's AuthenticationMiddleware populates scope["user"] on every connection but does not reject unauthenticated ones; HTTP endpoints opt into rejection through the @requires(...) decorator, and WebSocket endpoints have no equivalent. Marimo's maintainer diagnosed this in a September 2025 commit message and patched /ws and /ws_sync with a seven-line block. The /terminal/ws handler in the file next door got the identical seven-line block 211 days later, after it became CVE-2026-39987 and CISA KEV-listed. Five days after that, the same author patched the LSP ProxyMiddleware for the same class. Three instances, three fixes, one CVE. The CVE landed on the instance that happened to call pty.fork().

CVE-2024-45409: The SignatureValue Verified. The DigestValue Compared Was Not in the Signature.. Ruby-SAML's signature verifier has now produced authentication bypasses through four distinct mechanisms: duplicate-element wrapping (CVE-2016-5697), XML comment truncation (CVE-2017-11428), XPath root-anchoring (CVE-2024-45409), and parser differential between REXML and Nokogiri (CVE-2025-25291, CVE-2025-25292). The 1.18.0 fix landed six months after the 1.17.0 fix for this CVE. Each patch closed a specific instance. The architecture, a single validation method built on two XML parsers with XPath queries sprinkled through canonicalize-hash-compare-accept, is unchanged. The substrate that makes new variants exploitable holds across every fix.

CVE-2025-24367: cacti_escapeshellarg Escapes For a Shell That Never Runs. Cacti's rrdtool integration is a long-lived REPL: one rrdtool process per render, fed shell-quoted commands over stdin, sanitized at the argument boundary by a function written for the POSIX shell. The architecture has produced rrdtool-injection CVEs in waves. CVE-2025-24367 is one more instance against the same component. The 1.2.29 patch strips newlines from the per-argument escape and preserves every architectural choice that produced the family: the REPL design, the per-arg-only sanitization, the function whose name advertises a sink that is not in the call path. The component is a design-debt driver because the architecture, not the missing check, is what reliably reproduces the bug class.

CVE-2025-24054: The Two Verbs That Don't Trigger Are 'Open' and 'Execute'. The Windows shell's metadata-rendering pipeline is the design-debt-driver behind a recurring NTLM-hash-disclosure class: CVE-2024-43451 (.URL, November 2024, used as a zero-day against Ukrainian entities), CVE-2025-24071 (.library-ms folder-navigation, March 2025), CVE-2025-24054 (.library-ms selection-and-inspection, March 2025), CVE-2025-33053 (.URL WorkingDirectory=, June 2025). Different render paths, different researchers, same component, same primitive. Each patch closes a triggering condition; the substrate (shell parses metadata files and resolves embedded URLs at render time) is stable. The exhibit list of this pattern is the inventory of file formats whose URL element makes them what they are.

CVE-2026-39973: Apktool's Path Check Was Replaced With a Name Check. The Path Has Two Names.. brut/androlib/res/decoder/ResFileDecoder.java has shipped arbitrary-file-write via path traversal three times. CVE-2024-21633 (entry names, fixed 2.9.2), GHSA-vgwr-4w3p-xmjv (Windows variant, fixed 2.9.3), CVE-2026-39973 (type names, fixed 3.0.2). The 2024 fixes added BrutIO.detectPossibleDirectoryTraversal() on the composite output path; PR #4041 deleted it in December 2025 while adding a narrower per-entry-name validator; PR #4113 added a parallel per-type-name validator four months later. The design hasn't moved: the decoder still writes attacker-controlled strings into the output filesystem, and the gate that catches traversal is still per-input rather than per-result. The pattern's thesis ("patches address symptoms; the design holds the primitive") is the shape this file produces every cycle. Each fix targets the input the previous CVE came in on. The next input that gets added to the concat without its own validator will be the next CVE.

CVE-2026-5718: The Drag-and-Drop CF7 Nonce Mint Blocks One Caller, And Its Name Is curl. The drag-and-drop plugin's own changelog enumerates four consecutive file-upload CVEs: 1.3.9.0, 1.3.9.3, 1.3.9.6, 1.3.9.7. Each release plugged a specific bypass — extension blacklist, MIME spoof, ASCII-gated sanitizer, non-ASCII sanitizer — without ever asking why an unauthenticated handler mints upload nonces in the first place. The mint is the design debt. Every CVE in the series is a leaf on that root.

CVE-2026-26336: Alfresco's Path Blacklist Has Never Blocked ..;/. The Share resource controller has shipped a regex-blacklist defense against WEB-INF reads since at least 2021. CVE-2026-26336 is the bypass of the second pattern in that list; the 25.3.0 patch is, by inference from the public PoC succeeding against the patched build, the same shape of regex with one more entry. Each release closes a specific spelling of .. and leaves the design (string-inspection of pre-canonical URLs) untouched. The component's bug-class will recur until the design changes; the patches are proof of the debt, not its repayment.

CVE-2026-42222: Nginx-UI's /api/install Was on the Public Router. The Gate Was a Clock.. Nginx-UI's install-window gate is a design-debt driver in miniature. IsInstallTimeoutExceeded() plus the authIfInstalled shim were authored as a reusable convention for "endpoints that need to run before the wizard is done"; every pre-install feature reached for the same idiom. InitPublicRouter registered the install POST under the convention; api/backup/router.go registered the restore POST under the same authIfInstalled shim that imported system.InstallLockStatus and system.IsInstallTimeoutExceeded from the system package; the self-check router used the same shim. The class produced two consecutive CVEs (CVE-2026-42222, CVE-2026-42238) from the same gate in two endpoints, closed by one commit (25dea9f). The maintainer's fix is a substrate replacement (move every pre-install endpoint into a /setup group with an X-Install-Secret gate against a 0600 file) rather than a point fix on the original handler, which is the form a design-debt driver's eventual fix takes when the maintainer accepts that the architecture is producing the bug class faster than per-endpoint hardening can catch it.

CVE-2026-39363: Vite's /@fs Route Returns 403. The HMR WebSocket Returns the File.. server.fs.allow was bolted onto Vite years after the dev server's loader was written, as a response to earlier file-read CVEs in the HTTP path. Each new entry point into that loader (fetchModule over HMR, the Environment API in v6, the invoke RPC) inherits the original loader's permissive contract and has to remember to re-apply the allowlist at its own boundary. CVE-2026-39363 is the third or fourth time that bill has come due. The marimo terminal-WebSocket bug we covered on May 7 (marimo-terminal-ws-only-websocket-without-the-check) is the same shape one stack over: a feature added later than the auth model, on a transport the auth model never visited.

CVE-2026-21858: n8n's req.body.files Was Formidable's Output. The JSON Parser Used the Same Name.. n8n's getBodyData() is the substrate. The framework method returns whatever the upstream Express body parser wrote, with no record of which parser ran, and three handlers (Form/utils/utils.ts's prepareFormReturnItem, ChatTrigger.node.ts's handleFormData, Webhook.node.ts's file-handling method) read getBodyData().files on the same implicit assumption that the multipart parser populated it. The November 13, 2025 patch is one line of node:assert copied byte-for-byte into each of the three handler files plus four test-file mock updates. Each of the three insertions closes its handler. The substrate, a body-data API that does not name its producer, is unchanged. The next handler that reads getBodyData() body fields without the same assertion is the next entry into this CVE's bug class.

CVE-2026-24118: vm2's Patch Names Its Sibling. resetPromiseSpecies Was 2023.. The patch comment for CVE-2026-24118 cites resetPromiseSpecies in setup-sandbox.js by name and calls the new code "analogous." That citation is the maintainer naming a class. Two named defenses for the same primitive shipped 31 months apart: resetPromiseSpecies in commit 26168e6 (September 2023) for Promise.prototype.then, neutralizeArraySpeciesBatch in commit f9b700b (April 2026) for Array.prototype.map/filter/slice/concat/splice. The advisory thread for GHSA-grj5-jjm8-h35p documents seven PoCs for this CVE alone; PoCs #1-5 are descriptor-chain bypasses closed by intermediate fixes that never received CVE numbers, and PoC #7 graduated in from GHSA-55hx-c926-fr95 because the same primitive surfaced via Array.fromAsync. Each patch closes the entry point that produced its CVE. The substrate (a Proxy-mediated bridge whose traps cannot see C++ reads) is unchanged, and the next named defense joins the library whenever the next species-using built-in is reported.

CVE-2025-58179: Astro Shipped Two /_image Endpoints. Only One Read the Allowlist.. The Cloudflare adapter's image-endpoint.ts is twelve lines, by line count one of the smallest source files in the Astro repository. It absorbed two security commits in thirty-six days. The August 2025 commit added isRemoteAllowed and a 302-redirect-instead-of-proxy fallback. The October 2025 commit added an origin-equality check after URL resolution and refactored the underlying isRemotePath to defeat multi-level URL-encoding obfuscation. The October commit's "Have we been tricked into thinking this is local?" comment also landed in the canonical generic endpoint on the same day, meaning the substrate (an _image?href= proxy whose remote-vs-local decision rests on a single classifier) is producing the same bug class across both implementations. Each patch closed its instance. The substrate that produces "what counts as remote" parser disagreements is unchanged; the next CVE in this family will be whichever URL form the next researcher gets isRemotePath to misclassify.

CVE-2025-32432: Craft Patched the Caller. The Yii Fix Was Already Out.. Yii's Component::__set and Yii::createObject are the design-debt driver. The same primitive (as <name> keys triggering behavior attachment, __class overriding class during instantiation) produced CVE-2024-4990 in Yii itself and CVE-2025-32432 in Craft nine months later. The Yii 2.0.51 fix (July 18, 2024) added a Behavior-subclass guard that rejects __class values like yii\rbac\PhpManager. Craft kept its ~2.0.48.1 pin through the 3.9.15 patch and only bumped to ~2.0.54.0 in February 2026. Any Craft endpoint, plugin, or extension that fed body parameters into a Component constructor inherited the same primitive for the entire window.

CVE-2012-1823: The 2024 Patch Removed The Feature The 2012 Patch Filtered. PHP-CGI's bug family across cgi_main.c includes CVE-2012-1823 (the original argv-from-URL injection), CVE-2012-2311 (incomplete-fix bypass via wrapper shell scripts), CVE-2012-2335 and CVE-2012-2336 (additional argument-handling edges) all in the 5.3.13 / 5.4.3 cycle, and then CVE-2024-4577 twelve years later in the same code block. Six CVEs around one architectural choice: argv comes from the URL when there is no =. The 2012 fixes addressed instances; the substrate (the CGI SAPI honors RFC 3875 §4.4 indexed-query semantics for argv) was unchanged for twelve years, until the 2024 fix conceded the design's interaction with Windows char encoding could not converge and removed the substrate on Windows alone. Linux still ships the substrate. The next CVE in this family on Linux is whatever new way the URL→argv path can be reached.

CVE-2026-22812: OpenCode Patched the CORS, Not the Auth. The design holds the primitive. OpenCode's HTTP server runs unauthenticated by default; the patch in v1.0.216 added a CORS allowlist, not authentication. An optional OPENCODE_SERVER_PASSWORD flag landed three releases later in v1.1.15, and even there the auth middleware returns next() when the flag is unset. The default install of every subsequent release is still no auth. When a community contributor opened PR #9328 to flip the default to mandatory auth, the maintainer closed it within hours, citing backwards compatibility. SECURITY.md documents the position: "It is the end user's responsibility to secure the server." The next CVE in this codebase will exploit the same primitive, because the primitive is the design.

CVE-2025-54309: The AS2 Receiver Authenticated the Admin. CrushFTP composes seven inbound protocols (FTP, FTPS, SFTP, HTTPS, WebDAV, S3, AS2) onto a single shared session abstraction served through one WebInterface command surface. Each protocol is a seam where authority gets handed over to the shared session model. The CVE list is the seam list with dates: CVE-2024-4040 was the template-engine seam, CVE-2025-31161 was the cookie-format seam, CVE-2025-54309 is the AS2-dispatch seam. Three patches in fifteen months, three different mechanisms reaching unauthenticated admin on the same endpoint. The patches close instances; the substrate is the architecture that runs every protocol's authentication semantics through one mutable session slot. The next CVE is whichever protocol the next researcher or attacker reaches next.

CVE-2025-27407: graphql-ruby Loaded the Schema by Compiling It. The CVE-2025-27407 patch is a design-debt-driver admission written into a lint config. Two class_eval-with-HEREDOC sites had to be rewritten in one diff (InputObject#argument and BuildFromDefinition#define_field_resolve_method); both were instances of the same pleasant-to-write Ruby idiom for defining a method from a name. Robert Mosolgo's third change in the patch is a new RuboCop rule, cop/development/no_eval_cop.rb, that bans class_eval, module_eval, and instance_eval from the codebase regardless of arguments. The cop does not check whether the call passes a string or a block; it bans the method names entirely, on the read that any future contributor reaching for the same idiom will produce the same class of bug. Point-fixing the two known sites would have closed this CVE; banning the family is the maintainer's verdict that the design produces this bug class faster than review catches it.

Dirty Frag: the patched half is the half Ubuntu already mitigated. crypto_authenc_esn_decrypt writes attacker-controlled bytes into whatever the destination scatterlist resolves to, with no awareness of whether the page is read-only, page-cached, or backing a setuid binary. Copy Fail (CVE-2026-31431) reaches this sink via algif_aead. Dirty Frag (CVE-2026-43284) reaches it via MSG_SPLICE_PAGES plus xfrm. The fix is split across esp4.c, esp6.c, ip_output.c, and ip6_output.c precisely because the sink itself encodes no policy and every caller must encode it separately.

CVE-2026-40261: The Injection Is in syncCodeBase, Not generateP4Command. CVE-2021-29472 was command injection in Composer's Hg VCS driver via unsanitized URL strings. The 2021 fix hardened the Hg driver and "adjacent VCS drivers." The Perforce driver kept string-based shell command construction for another five years. Composer 2.9.6, which closes the Perforce driver CVEs, also separately hardens branch-name handling across git, hg, and fossil. The VCS driver architecture distributes shell command construction across independent files; the bug class reproduces driver by driver. This is the third distinct instance of the class across Composer's VCS drivers, with the Perforce driver reaching 2026 carrying the same primitive the Hg driver shipped with in 2021.

CVE-2026-34486: EncryptInterceptor Only Encrypts Messages That Survive Decryption. EncryptInterceptor has now produced a padding oracle (CVE-2026-29146) and a deserialization bypass (CVE-2026-34486) from the same messageReceived() method. The bypass was introduced by the padding oracle fix: refactoring the encryption manager relocated super.messageReceived() outside the try block. One security patch, one new CVE in the same file, same method, same code style. The component whose explicit job is securing cluster traffic has generated two consecutive CVEs from its primary security method. The patches closed the instances. The exception-handler style that produced both is unchanged.

CVE-2026-24120: vm2's README Already Admits the Bug Class Is the Design. The CVE descriptions across vm2's lineage rhyme on shape: proto walk, error-object constructor traversal, host-realm leak via Promise rejection, host-realm leak via wasm catch, host-realm leak via util.inspect callback, host-realm leak via property-descriptor enumeration. Every patch is a sanitisation layer at one boundary. The substrate, in-process JavaScript sandboxing through Proxies, is unchanged. v3.11.2's GHSA-9vg3 advisory documents a CVE introduced by an earlier advisory's hardening: the fix for one CVE produced the substrate for the next. Eighteen advisories closed across three months. The cadence does not trend down.

CVE-2026-28318: SolarWinds Patched The Path To The Decoder, Not The Decoder. SolarWinds Serv-U is a long-running file transfer codebase with HTTP, FTP, and SCP layered onto an architecture older than the threat model that now applies to it. CVE-2026-28318 is the third Serv-U entry in twenty-four months: CVE-2024-28995 (path traversal) landed on KEV in June 2024 after ransomware operators picked up Rapid7's exploit code; CVE-2025-40538 through 40541 patched four CVSS 9.1 pre-authentication root code execution bugs in February 2026; CVE-2026-28318 is the June 2026 KEV entry. The bugs are not the same bug; they share the substrate that produces them. The post widens the frame to the decoder layer: deflate decompression in HTTP servers has produced CWE-400 and CWE-122 findings against zlib, nghttp2, and MiniZip on a measured cadence, and the decoder that fails on attacker-controlled pre-auth bytes is the asset the class returns to.

CLFS: Ransomware's Favorite Kernel Driver. The Common Log File System driver has been the source of Windows kernel LPE CVEs for years, consistently, with the same general class (parser confusion on a binary log format that was designed for internal OS consumption and then exposed to user-supplied input). Ransomware crews maintain long lists of CLFS primitives because they know the pipeline: new CLFS CVE, ninety days to weaponization, fresh LPE for the next six months. Microsoft patches them. The next one shows up. The driver's parsing surface is too big and too untrusted for the patch-as-you-find-it model to converge.

Boundaries

Not every frequent-CVE component is a design-debt-driver. Popular libraries attract attention, which finds more bugs. If the CVEs are varied (one memory safety, one logic error, one auth bypass, one side channel), that is healthy security research, not pattern reproduction. Design-debt-driver requires the same CLASS showing up again.

Not every legacy codebase is a design-debt-driver. Old code has more bugs on average, because standards have moved and review practices have matured. A legacy system with diverse vulnerabilities is behind the times. A legacy system with the same vulnerability over and over is holding a primitive.

Not every misconfigurable product is a design-debt-driver. If operators keep setting things up unsafely, that is usability debt, and the fix is better defaults. The pattern here is internal: the component produces CVEs in its own code, not in how administrators wire it together.

Defender playbook

For each repeat-offender component in your stack, list its CVEs from the last three years side by side. If the descriptions rhyme (parser confusion, auth-gate failure, JMX surface, kernel-mode binary format) you are looking at a design-debt-driver. Budget for another one this year. It is coming.

Invest proportional to the design debt, not to the latest patch severity. A CLFS patch closes one flaw in a stream that has never stopped. Architectural alternatives, when they exist (kernel sandboxing, microkernel decomposition, component replacement), return more safety per dollar than patch velocity ever will.

Factor design-debt into build-vs-buy and renewal decisions. If you are buying a product built on a design-debt-driver, you are buying a subscription to its bug class. Price it accordingly, including the cost of patches, emergency deploys, and the occasional in-wild CVE gap.

Read advisories for the pattern, not the instance. A vendor announcement of "we fixed this specific thing" is often followed six months later by the same fix wording with a new number. The pattern in the advisory stream predicts the next CVE better than any signature.

Sandbox what you cannot rearchitect. You cannot fix someone else's design. You can shrink the blast radius of the next inevitable bug by putting the component in a container, a separate VM, or a capability-restricted mode where the primitive it produces is less useful.

Kinship

Unpatchable Primitive. The escalation shape. Design-debt-driver is patches keep coming AND the class keeps coming. Unpatchable-primitive is patches cannot close the primitive at all. A design-debt-driver that never converges eventually IS an unpatchable primitive.

Disclosure After Exploitation. Design-debt components are frequently first known through in-wild exploitation because the attackers also know the substrate. They find the next instance before the vendor does, and they use it, and the vendor learns about the class from the breach report.

Revocation Gap. The ongoing nature of design-debt-driver means new revocation gaps open on a predictable cadence. Each fresh CVE has its own window of in-wild exploitation before the patch, before the deploy, before the credentials sprayed during exploitation are rotated. The gaps compound across instances.

A design-debt-driver is not a CVE problem. It is a subscription.