422.41 XSS and CSRF

Protect software from two of the most common web-based attacks by validating input, managing sessions, and restricting unauthorised commands.

Overview

Cross-site scripting (XSS) and cross-site request forgery (CSRF) are two common and dangerous web-based attacks. They exploit the relationship between a user, their browser, and a trusted web application. Both rely on tricking users into performing unintended actions or injecting malicious code into otherwise trusted systems.

Understanding these attacks helps developers write more secure code and recognise vulnerable patterns in existing systems. While the technical implementation can be complex, the core concepts are straightforward and the prevention methods follow clear principles that you can apply to Flask applications like those you've built.

Learning Targets

In this topic, students learn to:

  • Explain how XSS and CSRF attacks exploit web applications

  • Identify scenarios where user input or session handling is vulnerable

  • Apply secure development techniques to prevent XSS and CSRF

  • Evaluate how cookies, tokens, and input validation improve web security

What is XSS (Cross-site scripting)?

XSS occurs when an attacker injects malicious scripts (usually JavaScript) into a web page viewed by other users. It happens when user input is not properly validated or escaped before being displayed in HTML templates.

Simple Flask Example

from flask import Flask, request

app = Flask(__name__)

@app.route('/welcome')
def welcome():
    # VULNERABLE: User input included directly in HTML
    username = request.args.get('username', '')
    return f"<h1>Welcome {username}!</h1>"

# If someone visits: /welcome?username=<script>alert('Hacked')</script>
# The JavaScript will execute in anyone's browser who views this page

Types of XSS

Stored XSS: The malicious script is saved in a database and served to multiple users

@app.route('/comment', methods=['POST'])
def add_comment():
    # VULNERABLE: Storing unsanitised user input
    comment = request.form['comment']
    
    # Save to database without validation
    cursor.execute("INSERT INTO comments (text) VALUES (?)", (comment,))
    
    return "Comment added!"

@app.route('/comments')
def show_comments():
    # VULNERABLE: Displaying stored content without escaping
    cursor.execute("SELECT text FROM comments")
    comments = cursor.fetchall()
    
    html = "<h2>Comments:</h2>"
    for comment in comments:
        html += f"<p>{comment[0]}</p>"  # Dangerous!
    
    return html

# Attack: User submits comment containing <script>steal_cookies()</script>
# Now every visitor to /comments has their session stolen

Reflected XSS: The script appears in a URL and is immediately reflected back

@app.route('/search')
def search():
    # VULNERABLE: Reflecting search terms without escaping
    query = request.args.get('query', '')
    return f"""
    <h2>Search Results for: {query}</h2>
    <p>No results found.</p>
    """

# Attack URL: /search?query=<script>document.location='http://evil.com'</script>
# Victim clicks link and gets redirected to attacker's site

DOM-based XSS: Client-side JavaScript processes user input unsafely

<!-- In your HTML template -->
<script>
// VULNERABLE: Using URL fragment directly in page
function showProfile() {
    var username = window.location.hash.substring(1);
    document.getElementById('profile').innerHTML = 'Profile: ' + username;
}
</script>

<!-- Attack URL: /profile#<img src=x onerror=alert('XSS')> -->

XSS Prevention Techniques

from flask import Flask, request, escape, render_template

@app.route('/welcome')
def welcome_secure():
    username = request.args.get('username', '')
    
    # SECURE: Escape user input before displaying
    safe_username = escape(username)
    return f"<h1>Welcome {safe_username}!</h1>"
    
    # escape() converts dangerous characters:
    # < becomes &lt;
    # > becomes &gt;  
    # " becomes &#34;
    # So <script> becomes &lt;script&gt; and won't execute

@app.route('/comments_secure')
def show_comments_secure():
    cursor.execute("SELECT text FROM comments")
    comments = cursor.fetchall()
    
    # SECURE: Use Jinja2 template with auto-escaping
    return render_template('comments.html', comments=comments)
<!-- comments.html template - Jinja2 auto-escapes by default -->
<h2>Comments:</h2>
{% for comment in comments %}
    <p>{{ comment[0] }}</p>  <!-- Automatically escaped -->
{% endfor %}

<!-- To disable escaping (dangerous!): {{ comment[0]|safe }} -->

Content Security Policy (CSP) Header:

from flask import Flask, make_response

@app.after_request
def add_security_headers(response):
    # Prevent XSS by controlling script sources
    response.headers['Content-Security-Policy'] = "default-src 'self'; script-src 'self'"
    return response

# This tells browsers:
# - Only load resources from the same domain
# - Only run scripts from the same domain  
# - Block any inline JavaScript

What is CSRF (Cross-site request forgery)?

CSRF tricks a logged-in user into submitting a request they didn't intend, usually by clicking a link or loading a page that sends a hidden request on their behalf. If the user is authenticated with a session cookie, the attacker's request appears valid to your Flask application.

How CSRF Works Against Flask Apps

# Your Flask banking app
@app.route('/transfer', methods=['POST'])
def transfer_money():
    # VULNERABLE: No CSRF protection
    to_account = request.form['to_account']
    amount = request.form['amount']
    
    # Check if user is logged in via session
    if 'user_id' in session:
        # Process transfer - this is dangerous without CSRF protection!
        process_transfer(session['user_id'], to_account, amount)
        return "Transfer successful"
    
    return "Please log in"
<!-- Attacker's malicious website -->
<html>
<body>
    <h1>You've won a prize! Click to claim:</h1>
    
    <!-- Hidden form targeting your Flask app -->
    <form action="https://yourbank.com/transfer" method="POST" id="evil">
        <input type="hidden" name="to_account" value="attacker123">
        <input type="hidden" name="amount" value="1000">
    </form>
    
    <!-- Form submits automatically -->
    <script>document.getElementById('evil').submit();</script>
</body>
</html>

<!-- 
When victim visits this page while logged into your app:
1. Browser automatically includes session cookies with the request
2. Your Flask app sees valid session and processes transfer
3. Money goes to attacker without victim knowing
-->

CSRF Prevention in Flask

Method 1: CSRF Tokens

from flask import Flask, session, request, render_template
import secrets

app = Flask(__name__)
app.secret_key = 'your-secret-key'

@app.route('/transfer_form')
def transfer_form():
    # Generate unique token for this form
    csrf_token = secrets.token_hex(16)
    session['csrf_token'] = csrf_token
    
    return render_template('transfer.html', csrf_token=csrf_token)

@app.route('/transfer', methods=['POST'])
def transfer_money_secure():
    # SECURE: Check CSRF token before processing
    submitted_token = request.form.get('csrf_token')
    stored_token = session.get('csrf_token')
    
    if not submitted_token or submitted_token != stored_token:
        return "Error: Invalid request (possible CSRF attack)", 403
    
    # Token is valid, safe to process
    if 'user_id' in session:
        to_account = request.form['to_account']
        amount = request.form['amount']
        process_transfer(session['user_id'], to_account, amount)
        return "Transfer successful"
    
    return "Please log in"
<!-- transfer.html template -->
<form action="/transfer" method="POST">
    <!-- Include CSRF token as hidden field -->
    <input type="hidden" name="csrf_token" value="{{ csrf_token }}">
    
    <label>To Account:</label>
    <input type="text" name="to_account" required>
    
    <label>Amount:</label>
    <input type="number" name="amount" required>
    
    <button type="submit">Transfer Money</button>
</form>

Method 2: SameSite Cookie Configuration

from flask import Flask
from datetime import timedelta

app = Flask(__name__)
app.secret_key = 'your-secret-key'

# Configure secure session cookies
app.config.update(
    SESSION_COOKIE_SAMESITE='Strict',  # Don't send cookies with cross-site requests
    SESSION_COOKIE_SECURE=True,        # Only send over HTTPS
    SESSION_COOKIE_HTTPONLY=True,      # Prevent JavaScript access
    PERMANENT_SESSION_LIFETIME=timedelta(minutes=30)  # Session timeout
)

"""
SameSite options:
- 'Strict': Never send cookies with cross-site requests (maximum protection)
- 'Lax': Send cookies with navigation links but not forms (balanced)
- 'None': Send cookies with all requests (least protection)
"""

Method 3: Using Flask-WTF Extension

from flask_wtf import FlaskForm, CSRFProtect
from wtforms import StringField, IntegerField, SubmitField
from wtforms.validators import DataRequired

# Enable CSRF protection app-wide
csrf = CSRFProtect(app)

class TransferForm(FlaskForm):
    to_account = StringField('To Account', validators=[DataRequired()])
    amount = IntegerField('Amount', validators=[DataRequired()])
    submit = SubmitField('Transfer')

@app.route('/transfer_wtf', methods=['GET', 'POST'])
def transfer_with_wtf():
    form = TransferForm()
    
    # CSRF token automatically validated by Flask-WTF
    if form.validate_on_submit():
        # Form is valid and CSRF token checked
        process_transfer(session['user_id'], form.to_account.data, form.amount.data)
        return "Transfer successful"
    
    return render_template('transfer_wtf.html', form=form)
<!-- transfer_wtf.html - CSRF token automatic -->
<form method="POST">
    {{ form.hidden_tag() }}  <!-- Includes CSRF token automatically -->
    
    {{ form.to_account.label }} {{ form.to_account() }}
    {{ form.amount.label }} {{ form.amount() }}
    {{ form.submit() }}
</form>

Key Differences Between XSS and CSRF

Feature
XSS
CSRF

Target

End users viewing the page

Web application's trust in user

Exploits

Unescaped user input

Authenticated user's session

Attack Method

Malicious scripts in content

Forged requests from external sites

Prevention

Input escaping, CSP headers

CSRF tokens, SameSite cookies

Impact

Script execution in browser

Unauthorised actions as legitimate user

Flask Protection

escape(), Jinja2 auto-escaping

Flask-WTF, session tokens

Code Interpretation Examples

# Example 1: Identify the XSS vulnerability
@app.route('/user/<username>')
def user_profile(username):
    return f"<h1>Profile for {username}</h1>"

# Problem: If URL is /user/<script>alert('XSS')</script>
# The script will execute in the browser
# Example 2: Secure version
@app.route('/user/<username>')
def user_profile_secure(username):
    safe_username = escape(username)
    return f"<h1>Profile for {safe_username}</h1>"

# Now <script> becomes &lt;script&gt; and won't execute
# Example 3: CSRF vulnerability
@app.route('/delete_account', methods=['POST'])
def delete_account():
    if 'user_id' in session:
        delete_user(session['user_id'])  # Dangerous without CSRF protection!
        return "Account deleted"
    return "Not logged in"

# Attacker can trick users into visiting a page that submits to this endpoint
# Example 4: CSRF protection
@app.route('/delete_account', methods=['POST'])
def delete_account_secure():
    # Check CSRF token first
    if request.form.get('csrf_token') != session.get('csrf_token'):
        return "Invalid request", 403
    
    if 'user_id' in session:
        delete_user(session['user_id'])
        return "Account deleted"
    return "Not logged in"

Real-World Examples in Your Flask Projects

XSS in a Blog App:

# If your project has a blog or comments feature
@app.route('/post/<int:post_id>')
def view_post(post_id):
    post = get_post(post_id)
    comments = get_comments(post_id)
    
    # SECURE: Use Jinja2 template (auto-escapes)
    return render_template('post.html', post=post, comments=comments)
    
    # VULNERABLE: Manual HTML construction
    # html = f"<h1>{post.title}</h1><p>{post.content}</p>"
    # for comment in comments:
    #     html += f"<div>{comment.text}</div>"  # XSS risk!

CSRF in User Settings:

# If your project has user account management
@app.route('/change_password', methods=['POST'])
def change_password():
    # SECURE: Include CSRF protection
    if not validate_csrf_token():
        return "Invalid request", 403
    
    old_password = request.form['old_password']
    new_password = request.form['new_password']
    
    # Process password change safely

Summary

  • XSS allows attackers to inject malicious scripts into web pages viewed by other users

  • CSRF tricks authenticated users into sending unauthorised requests to your Flask application

  • Flask provides tools like escape(), Jinja2 auto-escaping, and Flask-WTF for protection

  • Always validate input and include CSRF tokens in forms that change data

  • Use secure session configuration with SameSite cookies

  • These vulnerabilities are common in web applications but preventable with proper coding practices

Understanding these attacks helps you secure the Flask applications you build and identify potential vulnerabilities during development and testing.

Last updated

Was this helpful?