A race condition in cyber security is a flaw where the result depends on which of two requests finishes first: one coupon redeemed twice, one balance spent twice. It leads this list of 17 web vulnerability classes because it hides in ordinary checkout and credit code, and the fix is a database rule, not more application code.
What is a race condition in cyber security, and the words around it
A race condition in cyber security is a flaw where the outcome depends on which of two operations finishes first. In a web app it is a read, a decision and a write with a gap between them, which lets one coupon, credit or token be used more than once.
This page is one part of the web app security set: the named bug classes a scanner report or a pentest quote throws at you, sorted by whether an app built with an AI tool is likely to have them.
The pattern is the same every time. The code reads a value, decides something from it, then writes. A second request arrives in between, reads the same value before the first write lands, and makes the same decision. Neither request did anything wrong on its own, which is why a race condition vulnerability can survive code review and every single-user test. The race condition examples below are mine, picked because each one turns up in ordinary product code.
| Web-app race | The check | The use | What the attacker gains |
|---|---|---|---|
| Single-use coupon | Has this user redeemed the code? | Apply the discount and mark the code used | The same discount on several orders |
| Balance or credit deduction | Is the balance at least the price? | Subtract the price and deliver | Spending the same credit more than once |
| Capped signup offer (the first N accounts) | Is the count still under the cap? | Create the account with the offer | More accounts on the offer than the cap allows |
| Invite or password-reset token | Is the token unused and unexpired? | Act on it, then mark it used | One token accepted twice, by two sessions |
A race condition attack needs no special tooling: the attacker sends the same request many times at once and counts how many succeed. Serverless hosting adds a twist. Each request can run in its own instance, so a lock held in one process’s memory does not reach the others, even when the same mutex works on a laptop. That is my reading of how serverless hosting runs code, and it is why the fix belongs in the database. The class label is CWE-362, which MITRE’s CWE-362 entry names “Concurrent Execution using Shared Resource with Improper Synchronization (‘Race Condition’)”. Race conditions in cyber security come down to exactly this: shared state, and nothing forcing the second request to wait.
TOCTOU: time of check to time of use
TOCTOU means time of check to time of use: a race between checking a condition and acting on the result. MITRE lists it as CWE-367. The textbook case is a file swapped after a permission check; the web case is two queries where one statement should be.
The TOCTOU meaning is in the name: the gap sits between the time of check, when the condition is read, and the time of use, when the result is acted on. MITRE names the class “Time-of-check Time-of-use (TOCTOU) Race Condition” and describes a resource whose “state can change between the check and the use in a way that invalidates the results of the check”. Wikipedia’s entry defines it as a class of software bugs “caused by a race condition involving the checking of the state of a part of a system (such as a security credential) and the use of the results of that check”, and lists the spellings TOCTOU, TOCTTOU and TOC/TOU.
In practice the classic TOCTOU bug is a file: a program checks that the user may write to a path, and before it opens the file an attacker swaps the path for a link to something else. That is why the Wikipedia entry and the Linux teaching lab that rank for this term are Unix and file-system centric, though the same entry notes that TOCTOU race conditions “can occur in other contexts, including local sockets and improper use of database transactions”. In a web app, the TOCTOU vulnerability is a check in one query and a write in another, with the application trusting that nothing changed between them. A TOCTOU attack on it is the parallel-request test from the section above.
Every TOCTOU race condition is a race condition. Not every race is a security bug, though: two requests that both update a “last seen” timestamp race harmlessly. A TOCTOU race matters when the check guards money, access or a limit.
The vocabulary: vulnerability, exploit, payload
A vulnerability is the flaw, an exploit is the technique or code that uses it, and a payload is what runs afterwards. A scanner reports the first of those. Whether an exploit works against your app depends on whether the flawed code is reachable.
NIST’s glossary entry for vulnerability gives it as a “Weakness in an information system, system security procedures, internal controls, or implementation that could be exploited or triggered by a threat source.” NIST’s glossary has no standalone entry for “exploit”, so the next definitions are my own plain ones. An exploit in cyber security is the code or technique that turns a vulnerability into an outcome, and the payload is what it delivers once it is in: a stolen session, a shell, a changed record. A known exploit targets a flaw that already has a public advisory; a zero-day targets one the vendor has not fixed yet. An exploit chain links several small flaws, none serious alone, into one that is.
Exploitation in cyber security is the act of using an exploit, or a chain of them, against a live system, and the distinction matters to an owner because tools report vulnerabilities, not exploits. A computer security exploit needs a path from the internet to the flawed code, and a scanner finding can have no such path. In 3 of the 21 third-party apps I audited, scanners flagged 33 to 44 vulnerabilities, and I traced exactly zero of them as reachable. I audited those 21 in June and July 2026 as a selected set, not a random sample, so read it as a pattern in that set and not a rate for AI-built apps in general. The reverse also holds: code exploits against the logic and timing classes below need no scanner finding at all, since a scanner has no signature for them.
Why it matters for an AI-built app: the web application security vulnerabilities scanners miss
Web application security vulnerabilities fall into two groups: the ones a scanner can match, such as known-vulnerable packages and some injection, and the ones only a reader of the code finds, such as race conditions, logic flaws and misplaced trust in headers.
The Input, Injection & Abuse pillar averages 61.9 out of 100 across the 21 third-party apps I audited, scored on all 21. Those 21 apps went through my audits in June and July 2026, chosen for review rather than drawn at random, so the average describes that group and says nothing about every AI-built app.
The table below covers all 17 classes: how often I would expect each in an AI-built JavaScript app on Next.js, Supabase and serverless functions, what tends to find it, and the first check. The “realistic” and “what finds it” columns are my reading of that stack, not measured rates. Where a class is rare, the reason is in the cell; none of them is impossible.
| Class | Realistic in an AI-built JavaScript app | What finds it | First check |
|---|---|---|---|
| Race conditions and TOCTOU (CWE-362, CWE-367) | Often: checkout, credits, invites | Code read, parallel-request test | Fire one state change many times at once |
| Logic flaws and chaining | Often: prices and quantities trusted from the browser | Code read, manual test | Write each business rule as a sentence and break it |
| Server-side request forgery (SSRF) | Sometimes: only where the server fetches a user’s URL | Code read, manual test | Submit a private address to every URL field |
| Open redirect | Sometimes: login and logout flows with a next parameter | Scanner (some), manual test | Point the redirect parameter at another domain |
| Host header injection and cache poisoning or deception | Sometimes: reset emails and CDN caching rules | Manual test | Request a reset with a changed Host header |
| HTTP request smuggling (CWE-444) | Rarely: a managed platform runs the proxy chain | Version check | List every proxy you run yourself |
| DNS rebinding | Rarely in the deployed app: it targets URL fetchers and local dev servers | Code read | Check that the URL fetcher resolves once |
| Prototype pollution and CSPT (CWE-1321) | Sometimes: deep merges and fetch paths built from input | Dependency scanner, code read | Send a __proto__ key to a JSON endpoint |
| Framework advisories and GraphQL gaps | Sometimes: pinned framework versions, GraphQL defaults | Dependency scanner, manual test | Compare the framework version with its advisories |
| Code and command injection (CWE-94, CWE-78) | Sometimes: image, PDF and git tooling | Pattern scanner, code read | List every exec and eval |
| LDAP, XPath, template and CRLF injection | Rarely, except templates customers can edit | Code read | Find where user text is compiled as a template |
| XXE and entity expansion | Rarely: only where the app parses XML, such as SAML or SVG uploads | Code read | Upload XML that declares an external entity |
| Insecure deserialization | Rarely: JSON.parse returns plain data | Code read | Look for richer formats and signed-cookie secrets |
| Running untrusted code and sandbox escapes | Sometimes: formula, plugin and AI code features | Code read | Find every in-process sandbox |
| ReDoS and buffer overflows (CWE-1333, CWE-121) | Sometimes for ReDoS; buffer overflows only via native code | Linter, dependency scanner | Cap input length before every regex |
| Homoglyphs and malicious characters | Sometimes: public usernames and workspace names | Manual test | Register a look-alike of an existing name |
| Security misconfiguration | Often: settings chosen to make the preview work | Scanner, configuration review | Turn off debug output and review CORS |
Read the split down the third column. Dependency scanners and pattern matchers catch known-vulnerable packages and the obvious forms of vulnerabilities in code, such as a string passed to exec. They say little about the web app vulnerabilities that live in business rules, timing and trust: a race, a logic flaw, a header treated as truth. Those common web vulnerabilities only show up when someone reads the code or runs a test built for them, and the same holds for API security vulnerabilities, since an API endpoint is where most of these checks and writes happen. Web exploitation techniques against that second group leave nothing for a signature to match, which is why this page spends its words there.
Two classes are missing on purpose. SQL injection has its own page on SQL injection prevention, and cross-site scripting has one on how to mitigate cross-site scripting. The category-by-category walk of OWASP’s list, which maps each web application vulnerability to a test, is how to test the OWASP Top 10 vulnerabilities.
Logic flaws and vulnerability chaining
A logic flaw is code that does exactly what it was written to do, and that is the problem: a negative quantity, a browser-supplied price, a skipped step. A pattern scanner has no signature for it. Chaining joins two or more small flaws into one serious one.
My usual examples: a cart that accepts a negative quantity and credits the account, a checkout that trusts the price sent from the browser, a multi-step flow whose last step works without the earlier ones, and a plan downgrade that keeps the paid features. Each one is a correct implementation of a wrong rule, so there is nothing for a signature to match. That is my reading of why logic flaws are the class AI-built apps ship most confidently: the code looks finished.
Vulnerability chaining is how a list of “low” findings becomes a serious one. An open redirect is minor alone, and a login flow that puts a token in the URL is minor alone; together they can send the token to someone else’s server.
To look for them, write the rules of the business as plain sentences (“a coupon works once per account”, “a downgrade removes the premium features at the end of the period”) and then try to break each one with the app’s own screens and requests.
How it works: race conditions in a web app, and the fix
Race conditions are fixed in the database, in four ways: a unique constraint that rejects the second write, one conditional UPDATE instead of read-then-write, a transaction with a row lock, or an idempotency key for retries. A check in application code does not close the gap.
Here they are, strongest first. Use the first one that fits the data.
- 01 A database constraint that makes the second write fail, such as a unique index on the coupon and the user
- 02 One conditional statement instead of read-then-write: put the condition in the UPDATE WHERE clause and check the affected row count
- 03 A transaction with a row lock (SELECT ... FOR UPDATE) when several rows must agree before the write
- 04 An idempotency key when the caller may retry the same request
The constraint is the strongest because the database refuses the duplicate no matter how the requests interleave; unique indexes and the rest of that review are part of the database review checklist. The conditional statement is the usual fix for a balance: the same-row credit case, with the vulnerable and safe code side by side, is in the section on protecting money and counters from quiet corruption. When the rule spans several rows, a transaction takes a lock first; PostgreSQL’s row-level locks documentation says FOR UPDATE “causes the rows retrieved by the SELECT statement to be locked as though for update”, which “prevents them from being locked, modified or deleted by other transactions until the current transaction ends.” For a caller that retries, such as a payment webhook, the key pattern is the answer to how to make a webhook handler idempotent.
Three things do not close the gap, in my reading. A check in application code is the race itself. A debounce in the browser stops a double click and nothing else, because the attacker is not using your button. An in-memory lock does not reach the other instances on a serverless platform, for the reason in the first section. Rate limits narrow the window without closing it, since two requests are enough; the limits themselves are covered in rate limiting in an API.
The race condition example you can test on your own app is the first check in “How to check your own app” below.
The request-trust classes: when the app believes a URL, a header or a name
Seven classes share one mistake: the app treats something the client controls as a fact about the world. Each gets the same four things below: what it is, where it shows up in an AI-built app, the fix, and a safe test.
SSRF protection: what server-side request forgery is
Server-side request forgery makes your server fetch a URL an attacker chose, often an internal address or the cloud metadata endpoint. OWASP’s 2025 Top 10 maps it under A01, broken access control. SSRF protection is an allow list, or resolving the name and refusing private ranges and redirects.
An SSRF vulnerability exists wherever the server makes a request to a URL the user supplied. What makes a server-side request forgery vulnerability serious is where the server sits: inside the network, where it can reach addresses the attacker cannot, such as internal admin panels, databases with no password on a private network, and the metadata service that hands out cloud credentials. In OWASP’s lists, server-side request forgery had its own entry in 2021, as A10, and OWASP’s 2025 broken access control category names “CWE-918 Server-Side Request Forgery (SSRF)” among its notable CWEs.
Where it shows up in AI-built apps, from my list: link previews, “import image from URL”, webhook URL fields, PDF renderers that load remote assets, and AI features that browse. Any of those is an SSRF example waiting for a hostile URL.
OWASP’s SSRF Prevention Cheat Sheet splits the fix into two cases. When the app only needs to reach known, trusted destinations, validate the IP address or domain against an allow list, and “Do not accept complete URLs from the user because URL are difficult to validate”. When it must reach any external address, validate the format, resolve the name, check every returned address is public, allow only HTTP and HTTPS, and “don’t forget to disable the support for redirection in the web client used”. Both cases add network controls: a firewall and segregation that limit what the server can reach.
The safe test: submit a URL you control and read your endpoint’s request log to see exactly what the server sends. Then submit a private address and confirm the app refuses it. Evidence is the log entry and the refusal response.
Open redirect attack
An open redirect attack uses a parameter such as ?next= to send a visitor from your domain to any other site. It lends your domain to phishing links and can leak tokens in login flows. The fix: map a short name to the destination on the server, or accept allow-listed hosts only.
An open redirect is a ?next= or ?redirect= parameter the app follows wherever it points. An example of an open redirect, as an illustration rather than a working link: a login URL on your domain whose next parameter names another site, so the victim signs in on the real page and lands on a copy. OWASP’s cheat sheet on unvalidated redirects puts the risk plainly: “an attacker may successfully launch a phishing scam and steal user credentials.” On whether the Top 10 names it, OWASP’s 2025 A01 category lists “CWE-601 URL Redirection to Untrusted Site (‘Open Redirect’)” among the weaknesses it maps.
The fix, in the cheat sheet’s order: “do not allow the URL as user input for the destination”; where possible “have the user provide short name, ID or token which is mapped server-side to a full target URL”; and if input can’t be avoided, check it against a list of trusted hosts, “an allow-list approach, rather than a denylist.” One trap, in my reading: a “relative paths only” rule must also refuse a value that starts with two slashes, which the browser reads as another host.
The test: after login, set the parameter to another domain, then to the same domain written with two leading slashes and no scheme. Evidence is where the browser lands both times; a correct build stays on your site. Hosting rules can create the same hole outside your code, as the section on where Netlify’s protection stops shows.
Host header injection, cache poisoning and web cache deception
The Host header names the server and port a request is for; it is mandatory in HTTP/1.1 and fully controlled by the client. Host header injection abuses code that builds links from it, such as password-reset emails. Web cache poisoning stores a manipulated response for everyone; web cache deception gets one user’s private page cached publicly.
MDN describes the HTTP Host header as the request header that “specifies the host and port number of the server to which the request is being sent”, and gives Host: developer.mozilla.org as its example of the header. RFC 9112 makes it required: “A client MUST send a Host header field (Section 7.2 of [HTTP]) in all HTTP/1.1 request messages.” The Host and Origin headers answer different questions: Host says where the request is going, and Origin, in MDN’s words, carries the origin “that caused the request”.
The client writes the Host value, so an attacker can write any value. Code that builds a password-reset link or any absolute URL from the incoming host header can be made to email a victim a link to the attacker’s domain. The fix is to build URLs from a configured base URL, never from request headers, and let the platform reject hosts it does not serve. An “invalid host header” message from a local dev server is a different thing, covered under DNS rebinding below.
Cache poisoning in the web sense starts with an input that is not part of the cache key, such as a header or an odd parameter: it changes the response, and the cache then serves that poisoned copy to everyone. (The same words also name an attack on DNS resolvers, which is not this page’s topic.) Web cache deception runs the other way: a private page requested at a URL that looks static, such as /account/x.css, gets cached and served to the next visitor. Cloudflare’s guidance on cache poisoning says “Only cache files that are truly static” and “Do not rely on values in HTTP headers if they are not part of your cache key.” Its Cache Deception Armor is a cache rule that “will verify a URL’s extension matches the returned Content-Type.” My working rule on top: send Cache-Control: private on anything that differs per user.
Two tests. Request a password reset for your own account with a changed Host header, and read the link in the email you receive. Then, logged in, fetch an account page with .css appended, and fetch the same URL logged out or from another browser. The evidence is that the second response does not show the first user’s page, plus its cache header; fetching logged out only would pass even when the page is cached.
HTTP request smuggling
HTTP request smuggling happens when two servers in a chain disagree about where a request ends, so the tail of one request becomes the head of the next user’s. On a managed platform the provider runs the proxy chain; with your own proxy in front of Node, the patching is yours.
MITRE files request smuggling under CWE-444, “Inconsistent Interpretation of HTTP Requests (‘HTTP Request/Response Smuggling’)”, and gives examples of the disagreement: duplicate Transfer-Encoding or Content-Length headers, or a message whose Transfer-Encoding and Content-Length headers differ. The front server reads one length, the back server reads another, and the leftover bytes get glued onto whoever’s request comes next. HTTP Request Smuggler is the name of a testing tool for this class, not a separate bug.
On a managed host, HTTP smuggling is the provider’s problem. It becomes yours when you run your own nginx or a custom proxy in front of a Node server. My working rule there: keep both current and prefer HTTP/2 end to end. The realistic check for a small team is a list of every proxy it runs, with its version against the vendor’s advisories.
DNS rebinding and DNS exfiltration
DNS rebinding makes one hostname resolve to a public address when checked and an internal address when used. It defeats SSRF filters that validate the name only once and lets web pages reach local development servers. Resolve once, then connect to that address.
The DNS rebinding attack is a TOCTOU race against name resolution. An SSRF filter looks up the hostname, sees a public address and approves it; the HTTP client looks it up again a moment later and gets 127.0.0.1 or a cloud-internal address. The same trick lets a web page in the developer’s browser reach services on localhost. The server-side fix is to resolve the name once, validate that address, and connect to that address, not the name. On development servers the defense is a host allow list, which is my reading of what an “invalid host header” message from a dev server is about: the server refused a request for a hostname it was not told to serve.
DNS exfiltration is a different problem with a similar name: data leaves a network as lookups of attacker-controlled subdomains. It matters once an attacker already has code running, and it is a detection topic for your network, not a fix in your app.
Prototype pollution and client-side path traversal
Prototype pollution is a JavaScript flaw, CWE-1321, where a recursive merge fed user JSON writes to __proto__ and every object inherits the value. It often arrives through an outdated dependency. The fixes are the update and schema validation that rejects unknown keys.
The prototype pollution vulnerability is specific to JavaScript. A deep-merge helper or a path-based setter (set(obj, "a.b.c", value)) walks whatever keys the user sends, and a key of __proto__ writes onto the prototype every object shares. After that, a property like isAdmin can appear on objects that never had it. MITRE’s name is “Improperly Controlled Modification of Object Prototype Attributes (‘Prototype Pollution’)”. In my reading it usually arrives through a dependency rather than your own code, so the first fix is the update that the npm audit command points to, and the second is schema validation that rejects unknown keys, as in input validation in a web app.
CSPT, client-side path traversal, is user input placed inside the path of a fetch call, where a value containing ../ steers an authenticated request to a different endpoint than the one the code meant. The fix is to encode every path segment and validate ids as ids.
Next.js exploits and GraphQL vulnerabilities: two framework-shaped examples
The 2025 Next.js middleware bypass let requests skip authorization checks placed in middleware; where patching was infeasible, the advisory’s workaround blocks external requests carrying the x-middleware-subrequest header, and Vercel-hosted deployments were automatically protected. Three GraphQL gaps OWASP’s cheat sheet covers: introspection left on, unbounded query depth, and authorization checked at the route instead of each resolver.
A Next.js exploit that shows the pattern is CVE-2025-29927. The Next.js security advisory says “It is possible to bypass authorization checks within a Next.js application, if the authorization check occurs in middleware”, and its workaround is to “prevent external user requests which contain the x-middleware-subrequest header from reaching your Next.js application.” It adds that “Next.js deployments hosted on Vercel are automatically protected against this vulnerability.” The affected and fixed versions are listed in how the Next.js middleware bypass reached v0 apps, not repeated here.
In one app I audited, an AI coding workspace’s framework version had published advisories, none fixed inside its release line, so closing them needed a major upgrade, not a patch bump; the report also says the best-known middleware bypass was already patched at that version. The lesson I take from it: one headline patch does not clear a release line, and the upgrade path does. Keep the framework on a supported line, and check authorization again at the data layer, never only at the edge.
GraphQL vulnerabilities come from defaults and from where the check sits. OWASP’s GraphQL Cheat Sheet says to “Disable introspection queries system-wide in any production or publicly accessible environments”, to “Add depth limiting to incoming queries”, and to guard against batching, which it calls “a form of brute force attack”: many queries sent in a single network call, which “will likely bypass existing rate limits” in proxies and gateways that count raw requests. On authorization it says “Always validate that the requester is authorized to view or mutate/modify the data they are requesting”, with checks “on both edges and nodes”. The cheat sheet’s other controls are input validation, amount limiting, pagination, timeouts, query cost analysis, rate limiting and not returning excessive errors. The common AI-built version is a check written once at the route while each resolver trusts it. The tests: send the introspection query while logged out, then a deeply nested query, and read both responses.
The injection and parser classes: when input becomes instructions
Four classes turn data into instructions: input that reaches eval or a shell, a query or template language, an XML parser, or an object deserializer. SQL injection and cross-site scripting are linked above and not repeated here.
What is code injection, and command injection in Node
Code injection is user input reaching an interpreter such as eval, CWE-94. Command injection is user input reaching a shell, CWE-78. In Node the fix is execFile or spawn with an argument array and no shell, an allow list for program names and flags, and no eval.
MITRE’s names are “Improper Control of Generation of Code (‘Code Injection’)” and “Improper Neutralization of Special Elements used in an OS Command (‘OS Command Injection’)”. The different types of code injection are one mistake aimed at different interpreters: SQL, templates and the shell all run text as instructions, which is my framing, and SQL has its own page. A typical code injection example in Node is eval on a string that includes user input, such as a formula field, and an AI feature that runs a generated code sample the same way has a code injection hole by design. A command injection vulnerability in an AI-built app is usually child_process.exec with a filename or URL inside the string: image and PDF conversion, git operations, ffmpeg, and helper scripts the AI wrote around them. Command execution through a shell is what makes the difference, and Node’s child_process documentation spells it out: exec “spawns a shell and runs a command within that shell”, while execFile “does not spawn a shell by default”, and of exec it says “Never pass unsanitized user input to this function.”
Here is a command injection example in its simplest Node form, with the safe call under it.
// Vulnerable: the filename is read by a shell
exec(`convert ${file} out.pdf`);
// Safer: no shell, the filename is one argument
execFile('convert', [file, 'out.pdf']);
To prevent command injection, OWASP’s command injection defenses start with “Avoid calling OS commands directly”, then escaping, then “Parameterization in conjunction with Input Validation”, where commands “must be validated against a list of allowed commands”. For code, how to prevent code injection is the same rule applied to eval and new Function: my rule is none of either on anything a user can influence. OWASP’s page is the defensive command injection cheat sheet; command injection payloads are the attacker’s side, and this page gives none.
The test is a harmless stand-in for a command injection attack, run on your own staging app only: upload a file whose name contains a semicolon followed by a harmless command that writes a marker, and confirm the marker never appears. In PHP the same mistake is a user value passed to system(), which turns PHP command injection into remote code execution (PHP RCE) exactly as exec does in Node.
LDAP, XPath, template and CRLF injection
LDAP, XPath, template and CRLF injection are one mistake against four interpreters: building a query, a template or a header by joining strings. Server-side template injection is the one a SaaS meets, in customer-editable email templates, and it can lead to code execution.
The “where a small SaaS meets it” column is my reading.
| Interpreter | Where a small SaaS meets it | The fix |
|---|---|---|
| LDAP (directory queries) | Only with enterprise single sign-on against a customer’s directory | Escape every value with the LDAP encoding function, or use a framework that escapes |
| XPath (XML queries) | Rarely: XML-backed configuration or integrations | Parameterized XPath, never string joins |
| Server-side templates (SSTI) | Email and document templates customers can edit | A logic-less template engine for customer templates |
| CRLF (line breaks in headers and logs) | Redirect targets, custom headers, log lines | Reject carriage return and line feed in any value placed in a header |
LDAP injection is a directory query built from input. OWASP’s LDAP injection cheat sheet describes it as an attack on “web based applications that construct LDAP statements based on user input”, and its rule is that “you MUST escape any untrusted data that is added to any LDAP query.” An LDAP injection example in a small SaaS needs a directory login, which is why it is rare there. XPath injection is the same flaw against XML queries.
The SSTI abbreviation stands for server-side template injection, and OWASP’s testing guide says these vulnerabilities “occur when user input is embedded in a template in an unsafe manner and results in remote code execution on the server.” The SSTI meaning in a SaaS is concrete: a customer edits an email template, the app compiles it with a full-featured engine, and the template can reach the server. My working rule is to render customer templates in a logic-less engine that has no way to call code.
CRLF injection puts a carriage return and line feed into a value that lands in a header or a log line, which can split the response or forge a log entry. The shared fix for all four: never build the query, template or header by string concatenation; use the library’s parameterized or escaping form.
XXE and XML entity expansion
XXE is an XML parser resolving an external entity, which can read local files or make requests. OWASP listed it as A4 in 2017 and mapped it under security misconfiguration in 2021. Disable DTDs in every parser, including the ones behind SAML, SVG and spreadsheet uploads.
XML external entity processing starts with a feature of the format: a document can declare entities, and an external one points at a file or URL. A parser that resolves it reads that file into the document or makes that request for the attacker. XML entity expansion, the “billion laughs” case, nests entities until the parser runs out of memory. In OWASP’s lists, “A4:2017-XML External Entities (XXE)” became part of A05:2021 Security Misconfiguration, and the 2025 A02 category lists “CWE-611 Improper Restriction of XML External Entity Reference (XXE)” among its notable CWEs.
An XML file on its own is data; the harm comes from a parser that does what the file asks. That is my framing, and it is the point of XML security: secure XML handling is about the parser’s settings, not about scanning files. A JavaScript SaaS can still parse XML in several places, from my list: SAML responses, RSS and sitemap imports, SVG uploads, spreadsheet and document files (which are zipped XML), and payment and accounting integrations.
OWASP’s XXE Prevention Cheat Sheet gives the general rule: “The safest way to prevent XXE is always to disable DTDs (External Entities) completely.” Its per-parser settings cover C/C++, ColdFusion, Java, .NET, iOS, PHP and Python, with no Node library, so a Node app reads its own parser’s docs and learns which library parses each format. The test for an XXE vulnerability: upload XML whose DOCTYPE declares an external entity pointing at a URL you control. Evidence is a rejection, or a parse with the entity left unresolved and no request in your endpoint’s log; a parser that ignores external entities can accept the file and still be safe.
Insecure deserialization
Insecure deserialization turns attacker-supplied bytes back into objects in a format that carries behavior, such as PHP unserialize or Python pickle, and can run code. OWASP listed it as A8 in 2017. JSON.parse is not affected; signed client-held data with a leaked secret is.
OWASP’s Deserialization Cheat Sheet explains why native formats are the risk: they “usually offer more features than JSON or XML”, and those features “can sometimes be repurposed for malicious effect when operating on untrusted data”, with attacks that have allowed “denial-of-service, access control, or remote code execution (RCE)”. For PHP deserialization it says to check uses of unserialize() and to use “a safe, standard data interchange format such as JSON”; for Python it lists pickle loads as vulnerable. In the OWASP Top 10, insecure deserialization was A8 in 2017, and the 2021 introduction says “A8:2017-Insecure Deserialization is now a part of this larger category”, A08:2021 Software and Data Integrity Failures, while the 2025 A08 still lists “CWE-502: Deserialization of Untrusted Data”.
A JavaScript app mostly meets this through a library that uses a richer format, or through signed cookies and tokens whose secret is weak or has leaked, which is my reading. phpggc is a tool that builds PHP gadget chains; it is named here and not demonstrated. The fix: data-only formats, schema validation after parsing, and signatures on anything the client holds, with the secret kept out of the repository.
The runtime classes: when the app runs, matches or displays hostile input
Four classes are about the running app: what it executes, what it matches, what it shows to other users, and how it is set up.
Running untrusted code: vm2, isolated-vm and sandbox escapes
Node’s vm module is documented as not a security mechanism, and vm2’s own README warns that researchers keep finding ways to escape its sandbox. For untrusted or model-written code the dependable boundary is a separate process, container or microVM with no network and no secrets.
Apps that run user formulas, plugins or model-written code need a sandbox, and the first one people reach for is the wrong one. Node’s vm module warning reads: “The node:vm module is not a security mechanism. Do not use it to run untrusted code.”
vm2’s README is candid about its own sandbox escape history: “Despite our best efforts, researchers and security professionals continuously discover new ways to escape the vm2 sandbox”, and “vm2 should not be your only line of defense.” It lists stronger alternatives in this order: isolated-vm, which it describes as in maintenance mode; a separate process or worker threads with limited permissions; containers or VMs; and managed code-execution services. isolated-vm gives access to V8’s Isolate interface, and its own security section says “Use of isolated-vm to run untrusted code does not automatically make your application safe.” So, in my reading, no open source sandbox that runs inside the Node.js process is the boundary on its own.
My working rule: untrusted or model-written code runs in a separate process, a container or a microVM service, with no network access and no secrets in its environment. Python has no safe eval either, in my reading. Python’s ast documentation says ast.literal_eval evaluates only “a Python literal or container display”, then adds that “it is not free from attack” and “Calling it on untrusted data is thus not recommended.” Untrusted input there needs the same process boundary. AI features that execute generated code carry a second risk, the model being steered by its input, which is covered in what prompt injection is.
ReDoS and buffer overflows
Catastrophic backtracking is a regular expression taking exponential time on a crafted string, CWE-1333. In Node one such request can block the event loop for every user. Cap input length before matching and prefer a vetted validator or a linear-time engine.
MITRE names the class “Inefficient Regular Expression Complexity” and lists ReDoS among its alternate terms. Catastrophic backtracking in a regex comes from nested quantifiers: a pattern that can match the same text many ways tries all of them when the match fails. Node runs JavaScript on one thread, so in my reading one slow match stalls every request on that instance. It shows up in email and URL validators an AI wrote or copied. The fixes: a length cap before the regex, a vetted validator library, or a linear-time engine such as RE2, whose README says “the match time is linear in the length of the input string.”
Stack and buffer overflow bugs are writing past the end of a memory buffer, a C and C++ class; MITRE’s stack variant is CWE-121, “Stack-based Buffer Overflow”. A JavaScript app does not write stack-based buffer overflows. It inherits them through native dependencies and the runtime, so the control is patching.
Homoglyphs and malicious characters
A homoglyph is a character that looks like another, such as Cyrillic а for Latin a. Attackers use them for look-alike usernames and domains. Normalize names, restrict them to one script, and compare confusable skeletons before accepting a name as unique.
A homoglyph attack in a SaaS is a workspace called “Acme” spelled with one Cyrillic letter, inviting your customers’ staff, or a username that impersonates an admin. Invisible characters and bidirectional control characters do a related job: they hide text from reviewers. In typography the word only means similar-looking glyphs; here it is the security use. Unicode’s security mechanisms standard defines two strings as confusable “if and only if skeleton(X) = skeleton(Y)”, so the check is to compute each name’s skeleton and refuse a new name whose skeleton matches an existing one. The rest is my working rule: normalize names before storing them, restrict usernames to one script, and show punycode for links whose domain mixes scripts.
Misconfigurations as a vulnerability class
Security misconfiguration is A02 in OWASP’s 2025 Top 10: correct code behind an open setting, such as debug mode, permissive CORS or a public bucket. In my reading it is a class AI-built apps meet often, because builders pick settings that make the preview work.
OWASP’s 2025 security misconfiguration category moved up from fifth place in 2021 and describes the class as “when a system, application, or cloud service is set up incorrectly from a security perspective, creating vulnerabilities.” Misconfigurations are the class where the code can be right and the app still open. Security misconfiguration examples, my list, worded to OWASP’s description where A02 names the item:
- Default accounts whose passwords are still enabled and unchanged
- Debug mode left on in production
- Error handling that shows stack traces or other overly detailed messages to users
- Permissive CORS that lets any site call the API with credentials
- Cloud storage whose sharing permissions are open to the internet
- A database reachable with the public key because access rules were never written
Every item on that list is an example of security misconfiguration, and a misconfiguration attack needs no exploit code at all, only a request to the open setting. The full table of platform defaults for the tools AI builders use is in insecure defaults on managed platforms.
How to check your own app
Eight checks cover the classes an AI-built app realistically has: parallel requests at a state change, a foreign redirect, a forged Host header, a private URL, a .css suffix on an account page fetched logged in then logged out, a __proto__ key, an external-entity XML upload, and an exec inventory.
Test only systems you own, and run these on staging wherever a test can change data.
- 01 Fire the same state-changing request, such as a coupon redemption or a credit spend, about twenty times at once and count the successes; evidence: the success count, which should be one
- 02 Set a redirect parameter to another domain, then to the same domain written with two leading slashes and no scheme; evidence: where the browser lands both times
- 03 Request a password reset for your own account with a changed Host header; evidence: the domain of the link in the email
- 04 Submit a private address to every feature that fetches a URL; evidence: the refusal response
- 05 Logged in, fetch a per-user page with .css appended, then fetch the same URL logged out; evidence: the logged-out response carries no private content, and its cache header
- 06 Send unknown keys and a __proto__ key to a JSON endpoint; evidence: a validation error, or the stored record without those keys
- 07 Upload XML whose DOCTYPE declares an external entity at a URL you control; evidence: a rejection, or no request logged at your endpoint
- 08 List every exec, eval, new Function and template compile in the codebase and trace what reaches each; evidence: the list
The twenty-at-once figure is my working rule, a rough number and not a standard. Keep the request, the response and the date for each check, so a later fix can be compared with the same test. Check 6 overlaps with request validation as a control, which has its own page on input validation.
Where the sprint fits
In the Production Hardening Sprint, deliverable 3.1 validates every data-writing endpoint with explicit schemas and safe input/output handling, including injection defenses. Deliverable 3.7 reviews the application against the OWASP Top 10 and records findings, fixes, and evidence by category. Deliverable 3.10 tests the five highest-risk externally reachable attack surfaces, remediates findings, and delivers the methods and evidence, and AxonBuild performs this targeted test. The targeted security tests cover the five priority attack surfaces documented in the report. Formal third-party certifications and independent audit opinions are separate from the sprint deliverables. Every deliverable and its verify step is listed in the published scope.
Common questions about race conditions and other web vulnerabilities
Which is the correct solution to a toctou issue?
Make the check and the use one step that another request cannot interleave. In a web app that means a database constraint that rejects the second write, or one conditional statement that checks and writes at once; a row lock with SELECT ... FOR UPDATE covers the case where several rows must agree. Checking twice in application code only moves the gap.
What is the difference between a race condition and a deadlock?
A race gives a wrong result because nothing controlled the order of two operations. A deadlock gives no result, because two operations each hold something the other needs and both wait forever. Locks fix races and, used carelessly, cause deadlocks, so take locks in the same order everywhere.
What is SSRF vs CSRF?
SSRF tricks your server into making a request it should not; CSRF tricks a logged-in user’s browser into sending one. OWASP’s 2025 A01 names both, CWE-918 and CWE-352, among its notable weaknesses. Cross-site request forgery and its defenses have their own page in this series.
Is a Host header mandatory?
Yes for HTTP/1.1: RFC 9112 says a client MUST send one, and a server must answer a request without it with 400 Bad Request. HTTP/2 carries the same information in the :authority pseudo-header, and RFC 9113 says a client must not send a Host value that differs from it.
Should I enable DNS rebinding protection?
On a home or office resolver, yes, with exemptions. dnsmasq’s version rejects upstream answers in private ranges, which “blocks an attack where a browser behind a firewall is used to probe machines on the local network”. Its own exemptions show the cost: a domain that should resolve to a private address has to be allowed by name, and blocking 127.0.0.0/8 “may disable” real-time black hole list services. A router warning that says “potential DNS rebind attack detected” is, in my reading, this protection at work; if the domain is yours and should resolve privately, exempt it by name.
Is XML still used today?
Yes. SAML single sign-on, SVG images, office documents, RSS feeds and some payment formats are all XML, from my own list, which is why XXE still earns a place in OWASP’s 2025 misconfiguration category through CWE-611.
The checks in this guide show you where the app is open. The sprint below closes those gaps, tests the result and writes the evidence down.
Built it with AI. Now it has to hold up for real customers.
The Production Hardening Sprint takes the app you already have and builds the production foundation underneath it. Authentication and access rules, payments that stay consistent, error handling, monitoring, backups, automated tests and a documented handover. Our engineers work inside your existing codebase for ten working days. All 123 deliverables are included, and you get the evidence for each one.
See the Production Hardening Sprint →
$2,500 fixed price · 10 working days · One codebase