Chapter 13 · Part III · Software & the web

Injection: when data becomes code

SQL injection, command injection and cross-site scripting sound like three different problems. They are one problem wearing three uniforms. A program glues untrusted text into something that another program will parse as instructions, and the parser has no way to tell which part was meant as data. This chapter shows where that boundary sits, how ordinary text slips across it, and the handful of fixes that close it for good.

#1 & #2Cross-site scripting (CWE-79) and SQL injection (CWE-89) hold the top two places in MITRE's 2025 CWE Top 25
A05:2025Injection's slot in the OWASP Top 10 2025, down from A03 in 2021, and still the category with the most CVEs
37distinct weakness types (CWEs) OWASP files under Injection; the concept is identical for every interpreter

Common mix-up: injection is not fixed by "cleaning" input. Stripping apostrophes breaks the surname O'Brien and still leaves places where no apostrophe is needed. The fix lives at the exact point where data meets an interpreter: bind it as a parameter, pass it as a separate argument, or encode it for the precise spot in the page where it lands. Checking input is a useful second layer, never the first.

Before you start: no networking background needed. It helps to know that a web page is just text a server sends and your browser parses; if the request and response cycle is hazy, TCP & UDP on networks.jelia.nyc sets the scene.

The root cause

Every interpreter draws a line between code and data

Computers are full of small languages. A database speaks SQL. A Unix shell speaks its own command language, with pipes, wildcards and variables. A browser reads HTML, and inside the HTML it reads JavaScript, CSS and URLs, each with its own grammar. A directory server reads LDAP filters. A template engine reads its own little syntax of curly braces. Every one of these is an interpreter: a program that takes a string and decides what it means.

The first thing any interpreter does is tokenize. It walks the text one character at a time and cuts it into pieces: this is a keyword, this is a name, this is an operator, and this, between two quote marks, is a literal value that should be taken exactly as written. Those pieces are then arranged into a structure, a parse tree, and the structure is what actually runs. Quote marks, angle brackets, semicolons, spaces and backslashes are the characters that tell the tokenizer where one piece ends and the next begins. They are the interpreter's punctuation.

Injection happens when a program builds the text for an interpreter by gluing strings together, and one of the glued pieces came from somebody else. The program's author pictured the user's text sitting quietly in its slot. The tokenizer sees no slots, only characters. If the user's text contains the interpreter's punctuation, the tokenizer obeys it: a quote mark closes the literal early, a semicolon ends the command, an angle bracket opens a tag. Data has crossed the line and become part of the code. MITRE files the whole family under CWE-74, "improper neutralization of special elements in output used by a downstream component," which is a precise if unlovely way of saying the same thing.

Two points follow, and the rest of the chapter rests on them. First, the problem does not need an attacker. A customer called O'Brien will break a concatenated SQL query on day one, and a forum post about the <b> tag will turn a concatenated page bold. Attackers simply choose their characters on purpose. Second, the punctuation is different for every interpreter, and sometimes for different places inside one interpreter, which is why no single "sanitize" function can ever be right everywhere.

tokens(template ⊕ input) = tokens(template), one slot filled
The safety property in one line: whatever the input, the interpreter should see the same structure the programmer wrote, with the input occupying exactly one literal slot. Every fix in this chapter is a way of guaranteeing that equation instead of hoping for it.

Concatenation sends code and data down one channel and leaves the parser to guess where one ends. Parameterization uses two channels, so the guess never happens.

InterpreterIts punctuationWeaknessThe structural fix
SQL databasequotes, ;, --, keywordsCWE-89Parameterized queries; allow-lists for names
OS shellspace, ; | & $ ` > <, wildcardsCWE-78No shell: run the program with an argument array
Browser (HTML/JS/URL/CSS)< > & " ', backslash, %CWE-79Auto-escaping templates; context-aware encoding; CSP as a backstop
LDAP filter* ( ) \ and NULCWE-90Library filter builders; RFC 4515 escaping
Template engine{{ }}, {% %}, ${ }CWE-1336User text only ever as template data, never as template source
HTTP headers, log filescarriage return, line feedCWE-93, CWE-113, CWE-117Reject or encode line breaks; structured logging
Go deeper: why escaping by hand keeps failing

The tempting fix is to find the dangerous characters and put a backslash in front of them, or double them up. It can work, in exactly one context, for exactly one interpreter, configured exactly one way. The trouble is everything around that.

  • The rules differ. Standard SQL escapes a quote by doubling it; some databases also treat a backslash as an escape, depending on a server setting. A shell has three different quoting styles with different rules. HTML attribute values care about the quote character that surrounds them.
  • Not every slot is quoted. A number, a column name, a sort direction or a table name sits in the query with no quotes at all. Escaping quotes protects nothing there, as the tokenizer below will show you.
  • Character sets bite. Hand-written escapers that work byte by byte have historically been confused by multi-byte encodings, where a backslash they add ends up absorbed into the previous character.
  • Layers stack. A URL inside an HTML attribute inside a page is decoded twice, by two parsers, in a fixed order. Escaping for one and forgetting the other is a classic slip.
  • It is opt-in. Every concatenation site needs someone to remember. A codebase with a thousand queries needs a thousand correct memories. Parameterized APIs and auto-escaping templates make the safe path the default path, which is why OWASP ranks them first and hand escaping last.
CWE-89

SQL injection and the prepared statement

Here is the pattern that causes it, in a few lines of Python. It looks harmless, and with the name Smith it works.

# Vulnerable: the user's text becomes part of the SQL source
name = request.form["name"]
sql  = "SELECT id FROM users WHERE name = '" + name + "' AND active = 1"
cur.execute(sql)

Type O'Brien and the query text becomes … name = 'O'Brien' AND active = 1. The database tokenizes it, finds a string 'O', a stray word Brien, and an unterminated string that swallows the rest. The page crashes with a syntax error. A person choosing their characters deliberately can do more than crash it: they can rearrange the query so it asks a different question, reads other tables or, with a sufficiently privileged account, changes data. We do not need to show how; it is enough to see that the input decides the structure.

The fix is the parameterized query, also called a prepared statement or bind variable. You write the SQL once, with a placeholder where each value goes, and hand the values over separately.

# Fixed: the SQL is a constant; the value travels on its own
name = request.form["name"]
cur.execute("SELECT id FROM users WHERE name = ? AND active = 1", (name,))

The database parses the constant text first and fixes its shape: one placeholder, of type text, in the comparison. The value then arrives through a separate part of the database protocol, already labelled as data. It is never tokenized, so it cannot contain punctuation in any meaningful sense. An apostrophe in it is just an apostrophe. Every mainstream platform has this: PreparedStatement in Java, SqlParameter in .NET, ? or %s or :name placeholders in Python's database drivers, $1 in PostgreSQL clients. The placeholder style differs; the principle does not.

Three gaps remain, and they catch good teams.

  • Identifiers cannot be parameters. A placeholder stands for a value. Table names, column names, ASC and DESC are structure. If users choose a sort column, map their choice through an allow-list in your code ({"newest": "created", "price": "total"}) and only ever insert one of your own strings.
  • ORMs have escape hatches. An object-relational mapper builds parameterized SQL for you, until someone reaches for the raw-query feature: raw() and extra() in Django, text() in SQLAlchemy, FromSqlRaw in Entity Framework, native queries in JPA. Every one of them supports bind parameters; every one of them also accepts a string built with an f-string. OWASP's 2025 entry calls out ORM search parameters for exactly this reason.
  • Stored procedures are not magic. A procedure that takes parameters and uses them as values is safe. A procedure that glues its parameters into a string and executes that dynamically has the same bug, just moved inside the database.
# The ORM escape hatch, used badly
session.execute(text(f"SELECT * FROM orders WHERE note LIKE '%{term}%'"))
# Same feature, used with a bind parameter
session.execute(text("SELECT * FROM orders WHERE note LIKE :pat"), {"pat": f"%{term}%"})

Finally, assume one query somewhere will be wrong anyway, and limit what it could do. The account your web application uses to connect should have least privilege: read and write the tables it needs, no ability to drop tables, read other applications' schemas, or run operating-system commands through database extensions. A reporting page can connect as a read-only user. This turns a potential catastrophe into a contained bug.

Instrument 1

A query tokenizer you can break

This is a real tokenizer for a small SQL-like language, running in this page. Pick a query template, type into its slot, and switch between three ways of building the query. It never touches a database; it reports what the parser would see. Watch the token count. When it changes, your input has changed the query's structure.

Query template
Your input (the slot)

      
      

Template tokens–
Parser saw–
Structure change–
Input chars outside the literal–

Key: each box is one token, labelled with its kind. An orange bar under a box means its characters came from your input; a dashed outline with ✕ means input that escaped its literal and became structure; a green bar marks the slot as the template intended. Try O'Brien in all three modes, then the number and sort-column templates with hand-escaping.

Go deeper: what the tokenizer is doing

It scans left to right. Whitespace separates tokens and is dropped. A single quote starts a string literal that runs to the next single quote, where two quotes in a row ('') count as one literal quote, which is the SQL standard's escape. If the end of the text arrives first, the string is unterminated and the token is an error. Runs of letters become names, or keywords if they are on its short keyword list; digits become numbers; ? is a placeholder; =, <, > and friends are operators; and two hyphens start a comment that runs to the end of the line.

The structure test compares the kinds of tokens in your query with the kinds in the same template filled with a harmless sample. If the lists match and your input sits inside exactly one literal token that covers all of it, the structure is intact. Otherwise it reports how many tokens appeared or vanished. Real database parsers are far bigger, but they start in the same place, and this is the step where injection happens.

CWE-78

Command injection: skip the shell

Programs often need to run other programs: resize an image, convert a document, count the lines in an upload. The easy way in almost every language is a function that takes one string and hands it to a shell, such as system() in C and PHP, os.system or shell=True in Python, exec in Node's child_process. The shell then tokenizes that string with its own rules, and the shell's punctuation is generous: spaces split arguments; ;, && and | chain or pipe commands; $( ) and backticks run commands inside commands; * and ? expand to file names; > redirects output.

# Vulnerable: one string, parsed by /bin/sh
filename = upload.name
os.system("wc -l " + filename)

Give it a file called my report.txt and it counts lines in two files, my and report.txt, neither of which exists. That is the innocent version of the bug. Any shell punctuation in the name is obeyed the same way, which is how a file name becomes a second command.

The fix is not a cleverer string. It is to stop using a shell at all. Every operating system can start a program directly, given the program's path and a list of arguments. No shell is involved, so there is no shell tokenizer, and each list item reaches the program as exactly one argument, spaces and semicolons included.

# Fixed: no shell; each list item is exactly one argument
subprocess.run(["wc", "-l", "--", filename], check=True)

Notice the --. Even without a shell, the program itself parses its arguments, and most programs treat anything starting with a hyphen as an option. A file named -n would be read as a flag. The double hyphen is the widely used convention for "options end here; everything after is a file name." OWASP calls this argument injection, a close cousin of command injection, and the defense is the same in spirit: pass values in positions where they can only be values.

Better still, ask whether you need another program. Counting lines, resizing images and zipping files all have libraries in every mainstream language. A library call takes typed arguments and has no command language to inject into. When an external program is unavoidable, combine the argument array with an allow-list on what the value may look like (an upload ID that is 32 hex characters, say, mapped to a file path your code builds) and run it under an account that can touch nothing else.

Go deeper: the same bug in disguise
  • Hidden shells. Some APIs that look like they take an argument list still pass through a shell on certain platforms or when given a single string. Read the documentation of your language's process API for what it does with a string versus a list. Python's subprocess documentation, for example, has a dedicated section on security considerations.
  • Windows batch files. Starting a .bat or .cmd file on Windows runs it through the command interpreter, which has its own, different quoting rules. Avoid passing untrusted values to batch files.
  • Code injection (CWE-94). The same mistake one level up: building source code for eval(), a scripting engine or an expression language from user text. CWE-94 sits at number 10 in the 2025 Top 25. The fix is never to evaluate built strings; parse data with a data parser (JSON, not eval).
CWE-79

Cross-site scripting: the browser is the interpreter

A web page is a program that the server writes and your browser runs. When the server writes a visitor's comment, a search term or a display name into that program without encoding it, the browser's HTML parser decides what the text means, exactly as the SQL tokenizer did. A comment that mentions <b> turns the rest of the page bold. A comment carrying markup that loads script lets one visitor run code inside every other visitor's page, with that page's access to their account. That is cross-site scripting, or XSS, and it is the most reported weakness in the CWE data year after year. What a script running in your origin can then do with sessions and cookies belongs to the next chapter, Sessions, cookies, CSRF & CORS; here we stop it getting in.

XSS comes in three shapes, named for where the untrusted text travels.

The fix point differs: server-side templates for reflected and stored XSS, the page's own JavaScript for DOM-based XSS, which may never reach the server at all.

Reflected XSS bounces off the server: a search page that prints "No results for …" with the query pasted in. Stored XSS waits in the database for every future reader, which makes it the most damaging. DOM-based XSS happens entirely in the browser, when the page's own JavaScript takes something it read, from the address bar, a message from another window or local storage, and passes it to a sink that parses HTML or code.

// Vulnerable: the browser parses the comment as markup
li.innerHTML = "<b>" + author + "</b>: " + comment;
// Fixed: build elements; text goes in as text, never parsed
const b = document.createElement("b");
b.textContent = author;
li.append(b, ": " + comment);

Modern frameworks do this for you. React, Angular, Vue, Svelte and server-side engines such as Jinja2, Razor, Thymeleaf and Go's html/template encode every value they insert, by default. That default is the single biggest reason XSS is rarer in new code than it used to be. The danger is in the deliberately named escape hatches that switch it off, and in code review they deserve the same attention as a raw SQL query:

  • innerHTML, outerHTML, insertAdjacentHTML and document.write in plain JavaScript;
  • dangerouslySetInnerHTML in React, v-html in Vue, bypassSecurityTrustHtml in Angular, {@html} in Svelte;
  • |safe and mark_safe in Django and Jinja, Html.Raw in Razor;
  • eval, new Function, and setTimeout with a string.

When a feature genuinely needs user-supplied HTML, such as a rich-text editor, encoding is the wrong tool because it would show the tags as text. Use a well-maintained HTML sanitizer that parses the markup and keeps only an allow-list of harmless tags and attributes (DOMPurify is the widely used one in browsers), and keep it updated. Never write your own with regular expressions.

Output encoding

One string, five contexts

Here is the wrinkle that makes XSS subtle. A single HTML page contains several languages nested inside each other, and the browser switches parser depending on where it is. Text between tags is read by the HTML parser. A value inside an attribute is read by the same parser in a different mode, and the quote that surrounds it matters. Inside a <script> element, the JavaScript parser takes over, and the HTML parser only watches for the closing tag. Inside an href, the URL parser reads the value after the attribute is decoded. Inside a style, the CSS parser does.

Each of those parsers has different punctuation, so each needs a different encoder: a function that rewrites the special characters into a form the parser reads as plain data. HTML uses entities like &lt;. JavaScript strings use backslash escapes like \x3c. URLs use percent-encoding like %3C. CSS uses backslash hex like \3c . The OWASP cross-site scripting cheat sheet gives a rule per context. Use the wrong one and two things can go wrong: the reader sees garbage, or, worse, the value still breaks out because the encoder protected the wrong characters.

ContextExample slotEncoderNote
HTML text<p>…</p>HTML entities for & < > " 'What auto-escaping templates do by default
HTML attributevalue="…"Entities, ideally &#xHH; for all non-alphanumericsAlways quote attributes; unquoted ones end at a space
JavaScript stringconst v = "…"\xHH for all non-alphanumericsHTML entities are not decoded inside script; better to pass data via a JSON data attribute
URL parameterhref="/s?q=…"Percent-encoding, then it sits in an attributeNever let users supply a whole URL without checking its scheme
CSS valuestyle="color: …"\HH hex escapesPrefer an allow-list of values; CSS is a poor place for user data
Instrument 2

The output-context encoder

Type one untrusted string. The instrument writes it into five places on a page, using the encoder you choose for each, then runs the decoders the browser would run in that spot to work out two things: does the value stay inside its slot, and does the reader see what was typed? The markup is shown as text and nothing is ever rendered or executed. Start with "Right encoder everywhere," then try "HTML-encode everywhere," the most common half-fix.

Untrusted string
Safe and faithful–
Garbled for the reader–
Breaks out of its slot–

"Breaks out" means a character reached the parser as punctuation: it ended the attribute, opened a tag, closed the string or added a URL parameter. "Garbled" means the value stayed contained but the decoder for that spot does not undo the encoder you used, so readers see escape codes. "Wrong encoder" warns that the result is only fine for this particular input.

Go deeper: layered decoding, and why order matters

Some slots are read by two parsers in a row. A URL in an href is first decoded as an HTML attribute (entities become characters), then the result is parsed as a URL. So HTML-encoding an ampersand in a URL parameter does nothing for the URL: &amp; becomes & again before the URL parser sees it, and the ampersand starts a new parameter. The instrument models that order: attribute decoding first, then the URL or CSS decoder.

Script elements are the reverse surprise. The HTML parser treats a script's contents as raw text and does not decode entities, so &quot; inside a JavaScript string stays as six characters. Worse, the HTML parser is still scanning for the text that closes the script element, so a value containing it ends the script regardless of JavaScript quoting. That is why the JavaScript encoder escapes < and / too, and why the cleanest pattern is to keep data out of scripts entirely: put it in a data- attribute or a JSON block and read it with JSON.parse.

The backstop

Content Security Policy: a seatbelt, not a brake

Even a careful team will one day ship a page with one unencoded value. Content Security Policy (CSP), defined by the W3C's CSP Level 3 specification, is the browser feature that limits what that mistake can do. The server sends a response header listing which scripts, styles, images and frames the page is allowed to use. The browser enforces it, refusing to run anything else, and can send a report when it blocks something.

The most effective form is a strict CSP built on nonces. For every response, the server generates a fresh random value, puts it in the header and adds it as an attribute on each script tag it wrote on purpose. The CSP3 specification asks for at least 128 bits from a cryptographically secure random generator, new each time. A script that an injection bug slipped into the page cannot know the value, so the browser refuses it. A hash source does the same for a fixed inline script by listing the SHA-256 of its exact contents.

Content-Security-Policy: script-src 'nonce-{fresh random value}' 'strict-dynamic';
                         object-src 'none'; base-uri 'none'

'strict-dynamic' says that a script trusted through its nonce may load further scripts, which makes nonces practical with modern bundlers and tag managers; browsers that support it then ignore host allow-lists and 'unsafe-inline' in the same directive, leaving those as fallbacks for very old browsers. object-src 'none' shuts the door on plugin content, and base-uri 'none' stops an injected <base> tag from quietly redirecting every relative script URL on the page.

Older policies are often allow-lists of hosts: scripts from our own site and from cdn.example.com. They are easy to write and hard to make safe, because they trust every file on those hosts, including old library versions and endpoints that echo back caller-supplied JavaScript. Two keywords undo most of CSP's value: 'unsafe-inline' allows any inline script, including the injected one, and 'unsafe-eval' allows code built from strings.

Be clear about the limits. CSP does not fix the bug: injected markup can still deface the page, inject a convincing fake login form, or leak data through injected links and images unless other directives restrict them. It does nothing for SQL or command injection, which happen on the server. Deploy it in report-only mode first (Content-Security-Policy-Report-Only) to learn what would break. Pair it with Trusted Types where supported, a newer browser feature that makes DOM sinks like innerHTML refuse plain strings, so DOM-based XSS shows up as an error in testing instead of a hole in production.

Instrument 3

A CSP checker

Paste a policy, or pick a sample. The checker parses the directives the way a browser does, works out which one governs scripts, and flags the common weaknesses described above. It looks only at the text you give it.

Content-Security-Policy header value
Governs scripts–
Serious–
Worth fixing–
Good signs–
    Input checks

    Validation helps; encoding fixes

    If injection is about dangerous characters, why not refuse them at the door? Partly you should. Allow-list validation, which says what a value must look like rather than what it must not, catches junk early and narrows what can go wrong later. A quantity is a positive integer below 1,000. A country code is two letters from a known list. An order ID matches a fixed pattern. A sort column is one of three names. When a field has a shape like that, enforce it, on the server, every time.

    But most interesting fields have no such shape. Names contain apostrophes and hyphens. Addresses contain ampersands. A comment about programming will contain angle brackets. If validation rejected every character any interpreter considers special, it would reject legitimate customers, and it would still be wrong, because the same value later flows into a context with different punctuation. Validation cannot know where data will end up; output encoding and parameterization act at the moment it gets there.

    Blocklists, lists of forbidden words or patterns, are worse. They fail open: anything the list did not anticipate gets through, and interpreters have many equivalent ways to say the same thing. They also cause the very bugs they target, by mangling text in ways nobody intended. The OWASP Input Validation cheat sheet is blunt about it: denylisting is a mistake-prone, easily bypassed approach and should not be the main defense. The same goes for web application firewalls in front of an application: useful for buying time against a known bug, never a substitute for fixing the code.

    Go deeper: canonicalize, then validate

    When you do validate, do it on the value your code will actually use. Decode percent-encoding and Unicode normalization first (to a single canonical form), then check, then never decode again. Checking a value and then transforming it afterwards lets an encoded form slip past the check and turn into something else later. This matters most for file paths, where the related weakness is path traversal (CWE-22), and for URLs, where checking the scheme against an allow-list of https: and mailto: stops a user-supplied link from being a script URL.

    Same bug, other interpreters

    LDAP, templates, headers and logs

    LDAP injection · CWE-90

    Directory searches use filters such as (uid=jsmith). Glue a user name into the filter and characters like *, ( and ) change what the filter matches. Use your LDAP library's filter builder, or escape values as RFC 4515 specifies (each special character becomes a backslash and two hex digits), and bind with an account that can only read what the search needs.

    Template injection · CWE-1336

    Template engines are interpreters too, and many can reach into the host language. The bug is passing user text in as the template, for example letting users customize an email by writing template syntax, instead of as data rendered by a fixed template. If users must design templates, use a logic-less engine or a sandboxed mode with no access to objects, and treat that feature as code execution in your threat model.

    Header and CRLF injection · CWE-93, CWE-113

    HTTP headers and many text protocols end each line with a carriage return and line feed. A value containing those two characters, written into a header such as a redirect location or a cookie, ends the header early and lets the rest read as new headers. Modern frameworks reject line breaks in header values; if yours does not, reject them yourself.

    Log injection · CWE-117

    Write a user-supplied value containing a newline into a plain-text log and it can forge a whole extra log line that looks like it came from your system, or break the parser your monitoring relies on. Encode line breaks and control characters, or better, log structured records (JSON fields) so a value can never become a new entry. Chapter 17, Logs, detection & ATT&CK, is why this matters: your logs are evidence.

    Finding it before someone else does

    Testing and review

    Injection is unusually findable, because the dangerous patterns are visible in source code. Use several layers of tooling, because each sees what the others miss.

    • Static analysis (SAST) reads source code and traces data from sources (request parameters, headers, files) to sinks (query execution, process launch, HTML output). Most mainstream analyzers ship injection rules. Run them in every pull request so findings arrive while the author still remembers the code.
    • Dynamic testing (DAST) drives the running application from outside, sending unusual inputs to every parameter and watching for errors, timing differences and reflected output. It needs no source code and catches configuration problems, but only covers the pages it can reach. Run it against test environments you own, never against systems you do not.
    • Linters and framework settings can ban the escape hatches outright: a rule that fails the build on innerHTML or dangerouslySetInnerHTML unless a reviewer approves an exception, or Trusted Types enforced in the browser during tests.
    • Error handling matters too: a page that shows raw database errors to visitors is both a symptom and a tool for whoever is probing it. Log the detail; show the user a generic message.

    A code review checklist for injection

    1. Is any query, command, filter, template or markup built by gluing strings with values from outside the program? If yes, why is a parameter, argument array or auto-escaping template not used?
    2. Every raw-SQL or raw-HTML escape hatch (raw(), text(), innerHTML, |safe, v-html…): is each value bound or sanitized, and is there a comment saying why the hatch is needed?
    3. Identifiers, sort orders and table names chosen by users: are they mapped through an allow-list in code?
    4. Process launches: no shell, argument list, -- before positional values, least-privileged account?
    5. Values written into JavaScript, URLs or CSS: is the encoder the one for that context, or is the data passed through a data- attribute or JSON instead?
    6. User-supplied URLs: is the scheme checked against an allow-list?
    7. Database and service accounts: least privilege, no admin rights for the web tier?
    8. Is a CSP with nonces or hashes deployed, and do its reports reach someone?
    9. Do logs encode line breaks, and do error pages hide internals?
    Cheat sheet

    Terms from this chapter

    Injection
    Untrusted data reaching an interpreter in a way that lets it change the structure of a command, query or page instead of staying a value.
    Interpreter
    Any program that parses text and acts on its meaning: a database, a shell, a browser, an LDAP server, a template engine.
    Token
    One piece of text as a parser cuts it up: a keyword, a name, an operator or a literal value.
    Parameterized query
    A query written with placeholders and sent separately from its values, so values are never parsed as SQL. Also called a prepared statement.
    Argument array
    Starting a program with a list of separate arguments instead of a command string, so no shell parses the input.
    Cross-site scripting (XSS)
    Injection into a web page: untrusted text parsed by the browser as markup or script. Reflected, stored or DOM-based.
    Sink
    An API that parses or executes what it receives, such as innerHTML, eval or a raw-query call.
    Output encoding
    Rewriting special characters into the form a specific context reads as plain data: entities in HTML, \x escapes in JavaScript, percent-encoding in URLs.
    Content Security Policy
    A response header telling the browser which scripts and other resources a page may use; a backstop that limits XSS.
    Nonce
    In CSP, a fresh random value per response that marks scripts the server wrote on purpose.
    Allow-list
    A rule naming what is permitted; anything else is refused. The opposite of a blocklist, which fails open.
    Where this comes from

    Sources