hectoday
DocsCoursesChangelog GitHub
DocsCoursesChangelog GitHub

Access Required

Enter your access code to view courses.

Invalid code

← All courses Web Security Fundamentals with @hectoday/http

The Attacker's Mindset

  • Thinking Like an Attacker
  • Project Setup

Injection Attacks

  • SQL Injection
  • SQL Injection: Beyond the Basics
  • Command Injection
  • Header Injection

Cross-Site Scripting (XSS)

  • What Is XSS?
  • Output Encoding
  • Content Security Policy in Practice

Broken Access and Redirects

  • Insecure Direct Object References (IDOR)
  • Open Redirects
  • Server-Side Request Forgery (SSRF)

File and Data Handling

  • Path Traversal
  • Mass Assignment
  • Denial of Service via Input

Putting It All Together

  • Security Testing
  • The OWASP Top 10
  • Capstone: Hardened Notes API

Security Testing

Testing that your defenses actually work

You have added defenses throughout this course. But how do you know they work? How do you know they will keep working after the next code change?

Security testing has two parts: manual testing (trying attacks yourself) and automated testing (writing test cases that run on every change).

Manual testing with curl

Every attack in this course can be tested with curl. Here is a checklist:

SQL injection

# Should return only your notes, not all notes
curl -b cookies.txt "http://localhost:3000/notes/search?q=%27%20OR%20%271%27%3D%271"
# Expected: empty array (the injection is treated as a literal search string)

XSS (if you serve HTML)

# Should return encoded HTML, not executable script
curl -b cookies.txt http://localhost:3000/notes/NOTE_ID/view
# Check that <script> appears as &lt;script&gt; in the response

IDOR

# Alice should NOT see the admin's note
curl -b cookies.txt http://localhost:3000/notes/note-3
# Expected: 404 Not Found

Path traversal

curl -b cookies.txt http://localhost:3000/files/..%2F..%2Fetc%2Fpasswd
# Expected: 400 Invalid filename

SSRF

curl -b cookies.txt -X POST http://localhost:3000/bookmarks \
  -H "Content-Type: application/json" \
  -d '{"url":"http://169.254.169.254/latest/meta-data/"}'
# Expected: 400 URL not allowed

Mass assignment

curl -b cookies.txt -X POST http://localhost:3000/notes \
  -H "Content-Type: application/json" \
  -d '{"title":"Test","body":"Body","user_id":"admin-1"}'
# Then check: the note should be owned by alice (user-1), not admin-1

Automated testing

Manual testing is good for exploration. Automated testing ensures defenses survive code changes.

Using Node.js’s built-in test runner (or any test framework), you can write tests that call your app directly:

// tests/security.test.ts
import { describe, it, assert } from "node:test";
import { app } from "../src/app.js";

async function request(path: string, options?: RequestInit) {
  return app.fetch(new Request(`http://localhost:3000${path}`, options));
}

describe("SQL injection", () => {
  it("parameterized queries block injection", async () => {
    // Log in first
    const loginRes = await request("/login", {
      method: "POST",
      headers: { "content-type": "application/json" },
      body: JSON.stringify({ email: "[email protected]", password: "password123" }),
    });
    const cookie = loginRes.headers.get("set-cookie")!;

    // Try SQL injection
    const res = await request("/notes/search?q=%27%20OR%20%271%27%3D%271", {
      headers: { cookie },
    });
    const notes = await res.json();

    // Should NOT return the admin's note
    assert.ok(!notes.some((n: any) => n.user_id === "admin-1"));
  });
});

describe("IDOR", () => {
  it("users cannot access other users notes", async () => {
    const loginRes = await request("/login", {
      method: "POST",
      headers: { "content-type": "application/json" },
      body: JSON.stringify({ email: "[email protected]", password: "password123" }),
    });
    const cookie = loginRes.headers.get("set-cookie")!;

    const res = await request("/notes/note-3", { headers: { cookie } });
    assert.strictEqual(res.status, 404);
  });
});

The key pattern: test the attack, not just the happy path. A test that creates a note and reads it back verifies functionality. A test that tries to read another user’s note verifies security.

What to test

For each vulnerability type, write at least two tests:

  1. The attack fails: The defense blocks the malicious input
  2. Legitimate use works: The defense does not break normal functionality

This prevents both false negatives (the defense does not work) and false positives (the defense blocks legitimate requests).

Exercises

Exercise 1: Run through the manual testing checklist above. Does every defense hold?

Exercise 2: Write automated tests for IDOR and SQL injection using the pattern shown above. Run them with node --test.

Exercise 3: Add a test for mass assignment: create a note with user_id: "admin-1" in the body. Assert that the created note is owned by the authenticated user, not by admin-1.

Why should security tests check both attack failure AND legitimate use?

← Denial of Service via Input The OWASP Top 10 →

© 2026 hectoday. All rights reserved.