I started this learning project with a simple question: what is really happening between the moment I enter a URL and the moment a web page appears? The answer is a conversation between a client and a server, carried over HTTP. That conversation looks straightforward, but every request, header, cookie, and response creates decisions that affect security.
I also wanted a practical way to connect web fundamentals with the OWASP Top 10. The list is not a promise that an application is secure, but it is an excellent map of common failure patterns. While exploring it, I found that the same principles apply to newer systems too: an AI feature still has a browser, an API, authentication, data stores, dependencies, and permissions behind it.
This guide walks through HTTP, cookies, sessions, the OWASP Top 10:2021 categories, and three important OWASP risks for LLM applications. The examples are intentionally local and defensive. They are designed to make the trust boundaries visible without probing systems that you do not own.
Problem statement
Modern web applications are more than pages. A single browser action can involve a frontend, API, database, cloud service, third-party dependency, and sometimes an AI assistant. A small mistake—trusting a client-side permission check, concatenating input into a query, leaving debug mode enabled, or accepting an arbitrary server-side URL—can expose data or give an attacker control.
The web also has two properties that are easy to overlook:
- HTTP is stateless, so cookies and sessions must safely create continuity.
- The client controls the request, so URL parameters, headers, cookies, and request bodies are all untrusted input.
The OWASP Top 10 provides a common vocabulary for these risks. The LLM application risks add another warning: model input and model output are also untrusted. If model output is rendered as HTML, executed as a command, or placed into a database query without validation, familiar web vulnerabilities can return through a new interface.
Step-by-step: from a browser request to secure application behavior
1. Understand the client-server model and HTTP
A browser, mobile app, or API client is the client. The application server receives a request, applies business logic, accesses data or other services, and returns a response. HTTP defines the format of that request-response exchange.
HTTP is stateless: each request stands on its own. HTTPS is HTTP protected by TLS, which provides confidentiality, integrity, and server authentication while data travels over the network. In practice, HTTPS should be the default for every application.
A request commonly contains:
- A request line: method, path, and HTTP version.
- Headers such as
Host,User-Agent,Accept,Cookie, andAuthorization. - A blank line separating headers from the body.
- An optional body, often form data or JSON.
A response contains a status line, response headers, and an optional body:
POST /login HTTP/1.1
Host: example.test
User-Agent: Mozilla/5.0
Content-Type: application/x-www-form-urlencoded
Content-Length: 33
username=rahim&password=Secret123
HTTP/1.1 200 OK
Content-Type: text/html; charset=UTF-8
Set-Cookie: sessionid=8f2b91ac; HttpOnly; Secure; SameSite=Lax
Content-Length: 42
<html><body><h1>Welcome</h1></body></html>
The first digit of a status code indicates its class: 1xx informational, 2xx success, 3xx redirection, 4xx client error, and 5xx server error. For example, 401 means authentication is required, 403 means access is forbidden, 404 means the resource was not found, and 500 means the server failed. Production errors should not reveal SQL statements, filesystem paths, stack traces, or secrets.
You can inspect a public endpoint that you are allowed to access with curl:
curl -i https://example.com/
curl -I https://example.com/
curl -v https://example.com/
-i includes response headers, -I requests headers only using HEAD, and -v shows connection details. Use these commands only against systems you own or have permission to test.
2. Use HTTP methods deliberately
HTTP methods express the intended operation:
| Method | Purpose | Example |
|---|---|---|
GET |
Read data; should not change server state | GET /products/12 |
POST |
Create a resource or trigger an action | POST /orders |
PUT |
Replace a resource completely | PUT /users/7 |
PATCH |
Update part of a resource | PATCH /users/7 |
DELETE |
Remove a resource | DELETE /users/7 |
HEAD |
Return the headers of a GET response without its body |
HEAD /report.pdf |
OPTIONS |
Ask which methods or options are supported | OPTIONS /api/users |
A safe method is intended only for reading. An idempotent method produces the same final state when repeated. GET, PUT, and DELETE are generally idempotent; POST is not. That distinction matters for operations such as payments and order creation, where duplicate submissions need an idempotency key or server-side deduplication.
Do not implement a state-changing operation as a GET request:
# Unsafe design
GET /deleteUser?id=5
A link, image, crawler, or cross-site request could trigger it. Use an authenticated state-changing request instead, with authorization and CSRF defenses where cookie-based authentication is used:
curl -X DELETE 'https://example.test/api/users/5' \
-H 'Authorization: Bearer YOUR_TEST_TOKEN' \
-H 'Accept: application/json'
Never put passwords or other sensitive values in a URL query string. URLs can end up in browser history, access logs, proxy logs, bookmarks, and referrer data.
3. Read and set headers safely
Headers carry metadata and policy. Request headers include Host, User-Agent, Accept, Cookie, and Authorization. Response headers include Content-Type, Set-Cookie, Cache-Control, and Location.
Several response headers provide low-cost defense in depth:
Content-Security-Policy: default-src 'self'; script-src 'self'
Strict-Transport-Security: max-age=31536000; includeSubDomains
X-Content-Type-Options: nosniff
X-Frame-Options: DENY
Referrer-Policy: strict-origin-when-cross-origin
- Content-Security-Policy (CSP) limits where scripts and other resources may load from, reducing XSS impact.
- Strict-Transport-Security (HSTS) tells a browser to use HTTPS for the site.
- X-Content-Type-Options: nosniff prevents content-type guessing.
-
X-Frame-Options or CSP
frame-ancestorshelps prevent clickjacking. - Referrer-Policy reduces accidental URL information leakage.
Headers are not a substitute for authorization or output encoding, but missing security headers are a common form of security misconfiguration.
4. Understand cookies and session management
A cookie lets a stateless protocol carry a small piece of state. The server sends it with Set-Cookie; the browser returns it with Cookie on matching requests.
Set-Cookie: sessionid=random-server-side-id; HttpOnly; Secure; SameSite=Lax; Path=/; Max-Age=1800
Useful cookie attributes are:
-
HttpOnly: prevents JavaScript from reading the cookie, limiting some XSS damage. -
Secure: sends the cookie only over HTTPS. -
SameSite=LaxorStrict: reduces cross-site request forgery by restricting cross-site cookie sending. -
ExpiresorMax-Age: controls lifetime. -
DomainandPath: limit where the cookie is sent. Avoid unnecessarily broad domains.
Never store role=admin or other authoritative security decisions directly in an editable cookie. A safer session design stores only a long, cryptographically random identifier in the browser and keeps the actual session data on the server.
A secure session lifecycle should:
- Generate IDs with a cryptographically secure random generator.
- Rotate the session ID immediately after login to prevent session fixation.
- Apply
HttpOnly,Secure, and an appropriateSameSitevalue. - Use idle and absolute timeouts.
- Destroy the session on the server at logout.
- Re-authenticate before sensitive actions such as changing an email address.
Token-based systems such as JWTs follow the same core rules: protect the token, expire it, validate its signature and claims on every request, and design a sensible revocation strategy.
A minimal Flask example demonstrates secure cookie settings (the production app should also use a real server-side session store and HTTPS):
from flask import Flask, session
app = Flask(__name__)
app.config.update(
SECRET_KEY="use-a-secret-from-a-secret-manager",
SESSION_COOKIE_HTTPONLY=True,
SESSION_COOKIE_SECURE=True, # HTTPS only
SESSION_COOKIE_SAMESITE="Lax",
PERMANENT_SESSION_LIFETIME=1800,
)
@app.post("/login")
def login():
# Verify the submitted credentials first.
session.clear() # rotate/replace session state after login
session["user_id"] = "server-side-user-id"
session.permanent = True
return {"ok": True}
5. See why input must be treated as untrusted
The browser may perform client-side validation for a nicer experience, but an attacker can send requests directly. The server must validate and authorize every request, regardless of what the interface shows.
SQL injection: vulnerable and parameterized versions
String concatenation mixes data with SQL instructions:
# Vulnerable: never use this pattern
query = "SELECT * FROM users WHERE name = '" + username + "'"
A specially crafted value can change the meaning of the query. The general defense is a parameterized query, where the database driver keeps the value separate from the SQL structure:
# Safer: the value is bound as data, not interpreted as SQL
cursor.execute(
"SELECT id, name FROM users WHERE name = ?",
(username,)
)
Use prepared statements, safe ORM APIs, and allow-list validation for fields with a constrained format. Do not rely on blacklists alone.
Cross-site scripting: encode output for its context
If an application places untrusted text into HTML without encoding it, the browser may interpret the text as markup or script:
# Vulnerable template idea
return f"<h1>Search results for {query}</h1>"
Use a template engine's automatic escaping and keep untrusted values out of dangerous contexts:
<!-- Safer with a template engine's HTML escaping enabled -->
<h1>Search results for {{ query }}</h1>
Also deploy a restrictive CSP, avoid unsafe inline scripts, and set session cookies to HttpOnly. A local test can use harmless text such as <b>test</b> to confirm that the application displays it as text rather than interpreting it as markup.
Command injection and insecure LLM output
Never concatenate user-controlled text or model output into a shell command:
# Vulnerable
import os
os.system("convert " + user_filename + " output.png")
Prefer a library API or an argument list without a shell:
import subprocess
subprocess.run(
["convert", validated_input_path, "output.png"],
check=True,
shell=False,
)
The same rule applies to an LLM. Treat model output as untrusted input. Escape it before putting it in a page, parameterize it before putting it in a query, validate it against a strict schema before an API call, and never execute it as a shell command.
6. Work through the OWASP Top 10:2021
The OWASP Top 10 is published by the Open Worldwide Application Security Project, a nonprofit community that provides free security guidance. It is an awareness and prioritization tool—not a complete standard or a guarantee of safety. The following ten categories are the practical checklist I use when reviewing an application.
A01:2021 — Broken Access Control
What it is: The server does not correctly enforce what an authenticated user is allowed to view or change.
Typical failure: A user changes /invoice?id=101 to /invoice?id=102, or calls an admin endpoint directly after the UI button is hidden.
Impact: Data exposure, unauthorized edits or deletion, privilege escalation, and account takeover.
Prevention: Deny by default, check authorization on the server for every request, centralize authorization logic, do not trust client-supplied IDs, and log failed access attempts.
# Authorization must happen on the server, not only in the UI
invoice = get_invoice(invoice_id)
if invoice.owner_id != current_user.id and not current_user.is_admin:
return {"error": "forbidden"}, 403
A02:2021 — Cryptographic Failures
What it is: Sensitive data is not protected properly in transit or at rest.
Typical failure: Plain HTTP, plaintext or MD5 passwords, old algorithms, hard-coded keys, or unencrypted backups.
Impact: Stolen credentials and personal, health, or payment data, with legal and financial consequences.
Prevention: Use TLS everywhere, hash passwords with Argon2, bcrypt, or scrypt plus a salt, use modern cryptography, keep keys in a proper key store, rotate them, and avoid storing data that is not needed.
# Passwords should be hashed with a password-specific algorithm.
# Example with a library such as werkzeug.security:
from werkzeug.security import generate_password_hash, check_password_hash
stored = generate_password_hash(password, method="scrypt")
valid = check_password_hash(stored, submitted_password)
A03:2021 — Injection
What it is: Untrusted input is interpreted as part of a query, command, or markup.
Examples: SQL injection, command injection, LDAP injection, and XSS.
Impact: Authentication bypass, database theft or deletion, server command execution, or session theft.
Prevention: Parameterized queries, safe ORM methods, allow-list validation, context-appropriate output encoding, and automatic template escaping.
A04:2021 — Insecure Design
What it is: A weakness exists in the system's plan or business logic, even if the implementation is syntactically correct.
Typical failure: No login rate limit, an easy password-reset question, unlimited ticket purchases, or no abuse case considered for a feature.
Impact: Fraud, business-logic abuse, automated account attacks, and service abuse.
Prevention: Threat-model early, write abuse cases beside use cases, apply secure design patterns, and build limits and quotas into the design.
A05:2021 — Security Misconfiguration
What it is: A potentially secure system is deployed with unsafe settings.
Typical failure: Default credentials, directory listing, production debug mode, open unnecessary ports, missing security headers, verbose errors, or a public cloud bucket.
Impact: Easy entry and information leakage, sometimes leading to full compromise.
Prevention: Use hardened repeatable builds, remove unused features and sample applications, separate environments, change defaults, and scan configuration regularly.
A06:2021 — Vulnerable and Outdated Components
What it is: The application depends on libraries, plugins, frameworks, or server software with known vulnerabilities.
Impact: Public exploit code makes attacks inexpensive and fast.
Prevention: Maintain a dependency inventory or SBOM, remove unused packages, scan dependencies, follow advisories, and patch on a regular schedule.
# Example for a Python project: review installed packages
python -m pip list
# In a real project, add an approved dependency scanner to CI.
A07:2021 — Identification and Authentication Failures
What it is: The system cannot reliably establish or maintain a user's identity.
Typical failure: Weak or breached passwords, unlimited guessing, weak recovery, session IDs in URLs, or sessions that never expire.
Impact: Account takeover and identity theft, including administrator compromise.
Prevention: Multi-factor authentication, breached-password screening, login throttling and delays, generic login errors, and secure session management.
A08:2021 — Software and Data Integrity Failures
What it is: Code, packages, updates, or serialized data are trusted without verifying that they were not changed.
Typical failure: Untrusted CDN scripts, unsigned updates, unverified packages, or unsafe deserialization.
Impact: Supply-chain attacks and remote code execution.
Prevention: Verify signatures and checksums, use trusted repositories, use Subresource Integrity for external scripts, protect CI/CD, and avoid deserializing untrusted objects.
<script
src="https://cdn.example.test/library.min.js"
integrity="sha384-BASE64_HASH_OF_APPROVED_FILE"
crossorigin="anonymous"></script>
A09:2021 — Security Logging and Monitoring Failures
What it is: Attacks are not recorded, detected, alerted on, or investigated in time.
Impact: A breach can continue for months, and the organization may not know what happened or what was accessed.
Prevention: Log authentication events, access-control failures, validation failures, and high-value actions with useful context; centralize and protect logs; alert on suspicious patterns; and test incident response.
Do not log passwords, session tokens, or unnecessary personal data. A useful event might record a timestamp, actor ID, action, target ID, result, request ID, and source context without exposing secrets.
A10:2021 — Server-Side Request Forgery (SSRF)
What it is: A server fetches a user-supplied URL without adequate validation, allowing the server to make requests on the attacker's behalf.
Typical failure: An “import image from URL” or webhook tester accepts localhost, private network addresses, or cloud metadata endpoints.
Impact: Internal network access, cloud credential theft, internal scanning, and sometimes remote code execution.
Prevention: Allow-list domains, block loopback and private IP ranges, validate after DNS resolution, do not blindly follow redirects, isolate the fetching service, and return only the data the feature needs.
7. Extend the model to LLM application security
An LLM may sit between a user and documents, databases, APIs, or tools. It does not remove the classic OWASP risks; it adds a probabilistic trust boundary.
Prompt injection
Prompt injection occurs when input changes the model's intended behavior. Direct injection is typed by the user. Indirect injection is hidden in content the model reads, such as a web page, PDF, email, or code comment.
Possible effects include revealing system instructions, leaking private context, generating harmful or incorrect answers, or making unauthorized tool calls. For example, a malicious email could contain hidden instructions asking a summarization assistant to forward other messages.
Mitigations include separating trusted instructions from external content, clearly labeling external text as data, applying least privilege to tools, filtering and monitoring, red-team testing, and requiring a human approval step for payments, deletions, or external messages. There is no single perfect filter, so defense in depth matters.
Insecure output handling
Model output is influenced by user input and must be treated as untrusted. If it is inserted into HTML, SQL, a shell command, a file, or an API call without validation, it can become XSS, SQL injection, command execution, or an unsafe action.
A safer pattern is to constrain output to a schema and validate it before use:
from pydantic import BaseModel, Field
class SupportAction(BaseModel):
category: str = Field(pattern=r"^(billing|technical|account)$")
priority: int = Field(ge=1, le=3)
draft_reply: str = Field(max_length=2000)
# Parse and validate model output before calling any tool.
# Render draft_reply as escaped text; never treat it as HTML or code.
Training data poisoning
Training data poisoning occurs when an attacker deliberately adds bad data to training, fine-tuning, or retrieval sources. Poisoned data can introduce bias or factual errors, create insecure recommendations, or plant a trigger-based backdoor.
Defenses include vetted sources, provenance records, anomaly checks, isolated fine-tuning data, adversarial evaluation, monitoring after updates, and retraining when necessary. The same controls apply to documents used by retrieval-augmented systems.
8. Combine the controls into a security process
Security is a lifecycle rather than a final checkbox:
- Design: threat-model features, identify assets and trust boundaries, and write abuse cases.
- Develop: parameterize queries, encode output, enforce authorization server-side, protect sessions, and validate all inputs.
- Build: inventory dependencies, verify packages, protect CI/CD, and scan for known issues.
- Deploy: use HTTPS, secure headers, non-default credentials, least privilege, and separate environment configuration.
- Operate: centralize logs, monitor authentication and high-value actions, alert on anomalies, and maintain incident response.
- Test: review access-control cases, security headers, session behavior, dependency status, SSRF boundaries, and LLM prompt/output behavior in an authorized test environment.
Tools such as curl, browser developer tools, dependency scanners, and authorized security testing proxies can help inspect behavior. The source material for this project is theory-focused and does not prescribe a Burp Suite or OWASP ZAP lab; those tools are optional ways to observe the same request and response concepts in a local test application, not permission to scan third-party targets.
How to Verify
Use a small local application or an intentionally vulnerable training target that you own. Do not test public applications without explicit permission.
-
HTTP behavior: Run
curl -i,curl -I, andcurl -vagainst the local endpoint. Confirm the method, status code, response headers, and body match the endpoint's contract. -
Methods: Confirm that
GETdoes not change state, that aPOSTcreates only one record when an idempotency key is reused, and that unauthorizedDELETErequests return401or403. -
Cookies and sessions: Log in and inspect
Set-Cookie. ConfirmHttpOnly,Secure,SameSite, an appropriate lifetime, session rotation after login, timeout behavior, and server-side invalidation after logout. - Access control: Use two local test accounts. Confirm account A cannot read or edit account B's resource by changing an ID or calling the API directly.
-
Injection defenses: Submit harmless metacharacters and text such as
' OR '1'='1and<b>test</b>in the local application. Confirm SQL errors are not exposed, the database query remains parameterized, and markup is rendered as text rather than executed. -
Security headers: Check the response with browser developer tools or
curl -I. Confirm CSP, HSTS in the HTTPS deployment,nosniff, frame protection, and an intentional referrer policy. - Dependencies and configuration: Review the dependency inventory, remove unused packages, disable debug output, change defaults, and verify that production-like configuration does not expose directory listings or verbose errors.
- Logging: Trigger a failed login and a denied resource request. Confirm centralized logs contain useful event context but do not contain passwords or session tokens.
- SSRF boundary: In a local mock URL-fetch feature, confirm only explicitly allowed domains are accepted and loopback/private destinations and unsafe redirects are rejected.
- LLM controls: Use a mock model or test harness. Try direct and indirect prompt-injection strings, malformed structured output, and a poisoned test document. Confirm the application labels external text as data, validates output, limits tool permissions, records events safely, and asks for approval before high-impact actions.
What I Learned
- HTTP is simple because it is stateless, but that simplicity makes cookies and sessions security-critical.
- A request is client-controlled input, even when it comes from a normal browser.
- HTTP methods communicate intent; using
GETfor state changes creates avoidable risk. -
HttpOnly,Secure, andSameSiteare small settings with a large effect on session safety. - Authorization belongs on the server and must be checked for every protected resource.
- SQL injection and XSS are both instances of the same deeper problem: data was allowed to become code.
- The OWASP Top 10 is a prioritization map and shared language, not a complete security certification.
- Secure design and configuration can prevent entire classes of bugs before code-level fixes are possible.
- Logging only helps when events are recorded centrally, protected, reviewed, and connected to an incident process.
- LLM output should be treated like untrusted user input, even when it was generated inside my own application.
- Prompt injection, insecure output handling, and training data poisoning require least privilege, data provenance, validation, monitoring, and human review.
- Security is a continuing process: design, build, deploy, monitor, and test repeatedly.
Common Mistakes
| Mistake | Why It Happens | How to Fix |
|---|---|---|
| Trusting client-side role checks | The UI hides a button and appears to enforce permissions | Enforce authorization on every server-side request and deny by default |
| Putting passwords or secrets in URLs | Query strings are convenient to debug | Send sensitive data in a protected request body over HTTPS; never log secrets |
| Using string concatenation for SQL | It is quick in a small prototype | Use parameterized queries or safe ORM APIs |
| Rendering user or model text as HTML | Developers assume the source is trusted | Escape output for its context and use a restrictive CSP |
Storing role=admin in a cookie |
Cookies look like an easy place for state | Store only an opaque session ID; keep authoritative state server-side |
| Omitting cookie attributes | Browser defaults are misunderstood | Set HttpOnly, Secure, SameSite, narrow Path/Domain, and an intentional lifetime |
| Reusing a session ID after login | Session rotation is overlooked | Regenerate the identifier after authentication and sensitive privilege changes |
Using GET to delete or update data |
URLs are easy to link and test | Use the appropriate state-changing method with CSRF and authorization controls |
| Leaving debug mode or default credentials enabled | Development settings are copied into production | Separate configuration, harden builds, remove samples, and change defaults |
| Ignoring dependency updates | Updating feels risky or the inventory is incomplete | Maintain an SBOM/dependency inventory and patch through a tested schedule |
| Logging secrets or failing to log denials | Logging is added late without an event policy | Define safe security events, centralize logs, protect them, and alert on patterns |
| Letting a URL fetcher reach any address | The feature works in the happy path | Use domain allow-lists, block private/loopback ranges, validate redirects, and isolate the fetcher |
| Treating an LLM response as trusted code | The model is an internal component | Validate and encode output, use strict schemas, least privilege, and human approval |
| Assuming one prompt filter solves prompt injection | LLM behavior is probabilistic and context-dependent | Use layered controls: content separation, tool restrictions, monitoring, red teaming, and approval gates |
| Using unverified training or retrieval data | Public data appears automatically trustworthy | Vet sources, record provenance, check anomalies, and test models against adversarial cases |
Conclusion
Web application security starts with understanding the request-response cycle. Once it is clear that HTTP has no memory and that the client can alter every request, cookies, sessions, authorization, validation, and secure headers become practical necessities rather than abstract terminology.
The OWASP Top 10:2021 gives a useful map of the most common web application weaknesses: broken access control, cryptographic failures, injection, insecure design, misconfiguration, outdated components, authentication failures, integrity failures, logging failures, and SSRF. LLM application security extends the same thinking to prompt injection, unsafe model output, and poisoned data.
The most durable habits are straightforward: separate data from instructions, authorize on the server, protect sensitive data, keep dependencies and configuration under control, use least privilege, validate before acting, monitor what happens, and test only systems where you have permission. Those habits are useful whether the application is a simple website, a JSON API, or an AI-powered product.