Two of the most common web vulnerabilities are also the easiest to prevent. Learn the why, the how, and the exact fixes you should implement today.

Introduction
You have used fetch, built forms, handled database queries, and probably fixed bugs caused by unexpected input. But when a security issue shows up, many teams scramble, patch, and move on, still not truly understanding what went wrong.
Cross-site scripting (XSS) and SQL injection (SQLi) are not exotic problems. They are consequences of a single root cause: mixing code and data without proper boundaries. If you can clearly separate data from code and enforce that separation everywhere, you eliminate most opportunities for these attacks.
This article explains, in practical terms:
- What XSS and SQLi are, with the most common variants,
- How the attacks are crafted and why they succeed,
- Concrete, copy-paste mitigation patterns for common stacks,
- How to detect and test safely, and
- A final remediation checklist you can use in code reviews and CI.
Read it as a developer who wants to ship features and keep users safe.
Part 1: The Core Idea: Code vs Data
At a high level, both XSS and SQLi are the same mistake happening in different contexts:
- The application treats user-supplied data as executable code (SQL statements for SQLi; HTML/JS for XSS).
- Attackers craft input that changes the semantics of that code, causing unintended behavior.
If you keep a strict separation between data and executable content and use safe APIs that enforce that separation, these attacks are prevented by design.
Part 2: SQL Injection: What, Why, and How
What is SQL Injection?
SQL injection occurs when user input is interpolated into an SQL statement so that the input changes the intended query. The database executes the manipulated query, which can leak data, modify data, or allow remote code execution in extreme scenarios.
Common SQLi Types
- Error-based SQLi: The attacker causes the database to return an error that includes data or clues.
- Union-based SQLi: Injects
UNION SELECTto combine attacker-controlled result sets with legitimate results. - Boolean-based (blind) SQLi: Uses queries that return true/false to iterate and infer values.
- Time-based (blind) SQLi: Uses database sleep/time delays to infer information.
- Second-order SQLi: Attacker submits benign-looking data that becomes dangerous later when used unsafely.
Minimal vulnerable example (JavaScript, concatenation DO NOT USE)
// Insecure: do not concatenate input into SQL
const query = `SELECT * FROM users WHERE email = '${email}' AND password = '${password}'`;
db.query(query, (err, rows) => { ... });
If email or password contains characters like ' OR '1'='1, the condition can be manipulated.
Why it works
String concatenation mixes the SQL command and user data into a single token stream. The SQL engine cannot tell which part was intended as data and which as SQL.
Correct fixes (use parameterized queries / prepared statements)
Node.js with pg (Postgres):
const sql = 'SELECT * FROM users WHERE email = $1 AND password_hash = $2';
const params = [email, passwordHash];
const { rows } = await client.query(sql, params);
Node.js with MySQL using mysql2:
const sql = 'SELECT * FROM users WHERE email = ? AND password_hash = ?';
const [rows] = await pool.execute(sql, [email, passwordHash]);
PHP (PDO):
$stmt = $pdo->prepare('SELECT * FROM users WHERE email = :email AND password_hash = :hash');
$stmt->execute(['email' => $email, 'hash' => $hash]);
Prepared statements ensure the DB treats inputs strictly as data.
Additional SQLi controls
- Use ORMs/Query Builders (Sequelize, TypeORM, Knex) but watch raw queries.
- Avoid dynamic SQL when possible. If you must build dynamic queries, whitelist allowed identifiers (table/column names) before interpolating.
- Limit DB user permissions; the app account should have minimal privilege (no DROP, no schema changes).
- Disable verbose DB error messages in production leaking SQL errors helps an attacker.
- Use strict input validation (types, lengths, regex) as a defense-in-depth measure.
Part 3: Cross-Site Scripting (XSS): What, Why, and How
What is XSS?
XSS occurs when an application includes untrusted input in a web page without proper encoding or sanitization, allowing an attacker to execute JavaScript in other usersโ browsers. That script can read cookies, hijack sessions, perform actions on behalf of the user, or load malicious content.
XSS Variants
- Reflected XSS: Malicious input is reflected immediately by the response (e.g., search results).
- Stored (persistent) XSS: Malicious code is stored on the server (comment, profile) and served later to users.
- DOM-based XSS: Vulnerability lies in client-side code that inserts or evaluates input into the DOM insecurely (e.g.,
innerHTML,eval,location.hash).
Minimal vulnerable example (server-side reflected XSS)
// Insecure: inserting query param into HTML directly
app.get('/greet', (req, res) => {
const name = req.query.name || 'Guest';
res.send(`<p>Hello, ${name}</p>`); // vulnerable if name contains HTML/JS
});
If the attacker sends ?name=<script>steal()</script>, it runs for the user.
Why it works
Browsers interpret HTML and JavaScript. When untrusted data is written into a page without proper escaping, the browser executes it.
Correct fixes (contextual escaping and output encoding)
- Escape output based on context:
- HTML text node: escape
<,>,&. - HTML attribute: additionally escape quotes.
- JavaScript context: escape quotes, slashes appropriately.
- URL context: use proper URL-encoding.
2. Use frameworks that auto-escape:
- React (JSX) escapes by default when rendering expressions inside JSX.
- Templating engines (Handlebars, Mustache) often auto-escape.
3. Avoid innerHTML, document.write, or eval with user data. Prefer textContent / createTextNode or trusted DOM APIs.
Example safe rendering:
// Safe: create text node rather than injecting raw HTML
const p = document.createElement('p');
p.textContent = name; // browser will render as text not HTML
document.body.appendChild(p);
Additional XSS controls
- CSP (Content Security Policy): Reduce impact by disallowing inline scripts and only allowing trusted script sources.
// Safe: create text node rather than injecting raw HTML
const p = document.createElement('p');
p.textContent = name; // browser will render as text not HTML
document.body.appendChild(p);
- CSP is defense-in-depth; it is not a substitute for escaping.
- HTTP-only cookies: Mark session cookies
HttpOnlyso JS cannot read them even if XSS succeeds. - SameSite cookies: Mitigate CSRF / cross-site cookie attacks.
- Sanitize HTML only when absolutely needed: Libraries like DOMPurify can sanitize user-submitted HTML to a safe subset. Use with caution.
- Avoid storing unsafe input: Sanitize or strip risky content before database persistence if you must allow HTML.
Part 4: Practical Examples & Defenses (copy-paste ready)
Example 1: Express + Postgres Secure route
// Validate and sanitize input with zod, then use parameterized query
import { z } from 'zod';
const userSchema = z.object({
email: z.string().email(),
age: z.number().int().min(0).max(120).optional(),
});
app.post('/create', async (req, res) => {
const parse = userSchema.safeParse(req.body);
if (!parse.success) return res.status(400).send('Invalid input');
const { email, age } = parse.data;
const sql = 'INSERT INTO users (email, age) VALUES ($1, $2) RETURNING id';
const { rows } = await db.query(sql, [email, age || null]);
res.json({ id: rows[0].id });
});
- Input is validated with
zod. - Query uses parameterized placeholders (
$1,$2).
Example 2: React Safe rendering
function Comment({ text }) {
// text comes from user input
return <div>{text}</div>; // React escapes by default
}
If you must render HTML:
import DOMPurify from 'dompurify';
function SafeHtml({ html }) {
// Sanitize first, then use dangerouslySetInnerHTML
const clean = DOMPurify.sanitize(html);
return <div dangerouslySetInnerHTML={{ __html: clean }} />;
}
Only use sanitized HTML when you truly need rich content.
Example 3: PHP PDO prepared statement
$stmt = $pdo->prepare('SELECT id, email FROM users WHERE email = :email');
$stmt->execute(['email' => $_POST['email']]);
$user = $stmt->fetch();
Example 4: CSP header example (NGINX)
add_header Content-Security-Policy "default-src 'self'; script-src 'self' https://cdn.example.com; object-src 'none'; base-uri 'self';";
Part 5: Detection and Safe Testing
Responsible testing rules
- Only test systems you own or have explicit permission to test.
- For third-party systems, use bug bounty or coordinated disclosure channels.
- Never run automated exploit tools against production without authorization.
Tools you can use (for permitted testing)
- SAST (static): Semgrep, SonarQube, CodeQL catch unsafe string concatenations and insecure calls.
- DAST (dynamic): OWASP ZAP, Burp Suite detect reflective and stored XSS and some SQLi patterns.
- sqlmap powerful SQLi scanner (only on systems you are authorized to test).
- Dependency scanning: Snyk, Dependabot, to detect vulnerable libraries that might enable XSS or injection vectors.
- Fuzzing: Small controlled fuzz sessions for input endpoints.
- CSP reporting: Configure
report-uriorreport-toto collect CSP violations.
How to instrument tests into CI
- Integrate SAST (Semgrep/CodeQL) as part of PR checks.
- Run DAST (ZAP baseline scan) against staging environments on schedule or per deployment.
- Fail the build for critical security findings and create tickets for medium severity.
Part 6: Detection in Production
- Web Application Firewall (WAF): AWS WAF, Cloudflare, and ModSecurity can block common attack payloads at the edge. Do not rely solely on WAF.
- Runtime application monitoring: Log inputs that trigger errors, suspicious query patterns, or unusual parameter encodings.
- CSP reports: Used to detect inline script injection attempts and referrer-based anomalies.
- Error monitoring: Sentry, Rollbar watch for unusual stack traces or DB errors that could indicate probing.
Part 7: Hardening Checklist (for code reviews/release)
Apply this checklist before merging code that accepts or returns user input:
Input handling
- Validate shape, type, and length (server-side). Use strong validators.
- Enforce allow-lists for enumerations/identifiers (never allow arbitrary field names).
- Canonicalize input where necessary (normalize encoding).
Data access
- Use parameterized queries/prepared statements for every DB call.
- Avoid building SQL from user-supplied identifiers; if necessary, whitelist and map them.
- Use a least-privilege DB account.
Output handling
- Escape output for the exact context (HTML, attribute, JS, URL).
- Avoid inline JS; prefer external scripts and nonces with CSP.
- Use framework-native escaping when available.
Client-side
- Avoid
innerHTMLandevalwith untrusted data. - Use
textContentor safe DOM APIs. - Consider DOMPurify only if HTML must be supported.
Infrastructure
- Set
HttpOnlyandSecureandSameSitecookies where applicable. - Configure CSP with strict rules and reporting.
- Use a WAF with tuned rulesets for known patterns.
Testing
- SAST checks run on PRs.
- Regular DAST scans in staging.
- Add tests for input validation (unit tests that assert bad input is rejected).
Part 8: Real-World Case Studies (short)
Case: Stored XSS in comments
A web app stores user comments verbatim. Attackers posted <script> tags, and later, users who viewed the comments had their session tokens stolen by the script. Fix: sanitize/sanitize HTML on write and escape on read. Add CSP and mark cookies HttpOnly.
Case: SQLi in search bar
Search query was interpolated into a WHERE clause to support ORDER BY and dynamic filters. Attackers used SQLi to extract user data. Fix: validate and whitelist ORDER BY/ASC|DESC options; use parameterized queries for values.
Part 9: Common Developer Mistakes & How to Avoid Them
- Reliance on client-side validation only: Client-side validation is usability, not security. Always validate server-side.
- Assuming frameworks make you safe: Many frameworks escape by default, but developers introduce raw queries,
dangerouslySetInnerHTML, or calling OS shell commands reintroduces risk. - Putting HTML sanitization logic in many places: Centralize sanitization utilities with tests.
- Leaving detailed DB errors enabled in production: Hide errors from users and log them securely for developers.
Conclusion: Build Safety into Your API and UI
XSS and SQL injection are not clever hacker-only problems. They are the predictable result of treating external data as code. The fix is straightforward and repeatable:
- Treat all external data as untrusted.
- Enforce validation server-side.
- Use safe APIs (parameterized queries, safe templating, and DOM APIs).
- Use defense-in-depth: escaping, CSP, HttpOnly cookies, least privileged DB users, WAF, and continuous testing.
Developers who internalize this mindset ship features that survive real-world usage and attacks. That is the real difference between โworking softwareโ and resilient, secure systems.
Call to Action
Which parts of your codebase accept raw user data without validation or safe APIs? Run a quick grep for string concatenation into SQL, innerHTML, eval, and shell invocations, then fix them this sprint. If you want, paste a short snippet youโre worried about (without production secrets), and Iโll walk through exact fixes.
Responsible testing reminder
Only test for XSS or SQLi on systems you own or where you have explicit permission. Unauthorized testing is illegal and unethical. If you find a vulnerability on a third-party site, use their bug bounty or responsible disclosure process.


Leave a Reply