Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Break It, Then Fix It: An OWASP Top 10 Workshop...

Break It, Then Fix It: An OWASP Top 10 Workshop for PHP Developers

Reading about injection teaches you the words. Exploiting it teaches you the lesson. This hands-on tutorial walks every OWASP Top 10 category inside a deliberately vulnerable PHP app—you'll find the bug, exploit it yourself, then fix it properly. We cover injection, broken access control, authentication failures, SSRF, insecure deserialization, and the rest, with concrete mitigations for Laravel,

Symfony, and WordPress. You leave with muscle memory and a hardening checklist—not just awareness.

Avatar for Eric Mann

Eric Mann

October 12, 2026

More Decks by Eric Mann

Other Decks in Technology

Transcript

  1. Break It, Then Fix It An OWASP Top 10 (2025)

    Workshop for PHP Developers Presenter What This Is Eric Mann A hands-on, exploit-first workshop. You will break a deliberately vulnerable PHP Architect Security Corner PHP 8.3/8.4 Release Manager PHP application called Rampart, then fix every hole you open — one OWASP category at a time. What You'll Walk Away With A working mental model of the OWASP Top 10 (2025), a set of regression tests you wrote yourself, and a hardening checklist you can apply to production code on Monday.
  2. Ground Rules Sandbox Only Your Copy, Your Attacks The Job

    is Defense This app is deliberately vulnerable. Run it Attack only your own copy. What happens What you learn here is for defense. The only in the provided Docker sandbox — in your container stays in your container. exploits are the easy part; the job is fixing never expose it to a network you don't own. them.
  3. What You Need Running 01 02 03 Docker on Your

    Laptop Clone & Compose Up Two Test Commands Any recent version of Docker Desktop or Docker git clone the repo Engine will do. This is your only hard dependency. app running on localhost. One line to a working composer test:exploits — mine. More on that at target. the end. → make build && make up → composer test — yours to extend all day.
  4. Meet Rampart What It Is The Name Rampart is a

    customer-support & ticketing SaaS. Customers file tickets, A rampart is a fortification. This one doesn't hold. agents reply, admins run the show. It looks like a real product. It even has tests. It is also full of holes — on purpose. Roles in the App Customer — files and views their own tickets Agent — replies to tickets, sees the queue Admin — runs the show, manages users Your Mission Rename it if you ship your own fork. Break it first.
  5. How Today Works — The Loop Brief Exploit Your Turn

    Fix It Live CrossFramework The Public Test Suite Regression Tests The composer test suite proves the app still works after your fix. Run it Add a regression test for every bug you close — fails before the fix, passes after every patch. after. This is how you prove you actually fixed it.
  6. Threat Modeling in 90 Seconds The Right Question Use Cases

    Have Twins Don't just ask "what should this do?" Every user story has an abuse case living next to it. The attacker reads Ask: "What's the worst thing someone could make it do?" your spec and asks the evil version of every question. The Workshop Lens Every screen today has a "what's the destructive option in this dropdown?" question hiding in it. That's the question we're training you to ask automatically.
  7. OWASP TOP 10 · 2025 A01: Broken Access Control #1

    in 2025 The most common serious flaw on the internet — and in 2025 it also absorbs Server-Side Request Forgery (SSRF). If your app doesn't rigorously check who is allowed to do what to which object, everything else you build is compromised.
  8. A01 — Brief IDOR / BOLA Privilege Escalation SSRF Read

    anyone's ticket by changing the ID in Mass-assign yourself role=admin via a Paste http://169.254.169.254/latest/meta- the URL. The app checks "is logged in" — not profile update. The app trusts every field in data/ into the "preview link" field. The server "is this your ticket." the request body. fetches cloud metadata on your behalf. The most common serious flaw on the internet. Now also absorbs SSRF in the 2025 list.
  9. A01 — Exploit 1 IDOR 2 Mass Assignment / Privesc

    3 SSRF Log in as a normal customer. Change the Update your profile and include an extra Paste ticket ID in the URL. You are now reading field: role=admin. The app trusts it. You http://metadata.internal/latest/meta- someone else's ticket. No error. No are now an admin. data/ into the "preview link" field. The warning. server fetches cloud metadata for you — credentials included.
  10. A01 — Your Turn (7 min) The Challenge Find and

    close one of: The IDOR (ticket read by ID) The privilege escalation (mass assignment) The SSRF (preview link fetch) Hint cards are on the tables if you stall. Definition of Done The exploit no longer works The public test suite still passes (composer test) You've added a regression test that fails on the unfixed code and passes on yours
  11. A01 — The Fix IDOR / BOLA Privilege Escalation SSRF

    Authorize on the object, not just "is logged Stop mass assignment. Use explicit Allowlist destinations. Block private and in." Use Laravel Policies / Gates on every $fillable. Never use $guarded = []. Never link-local ranges. Disable file:// and read & write. The question is not "are you accept role from the request body — set it gopher:// schemes. Resolve the hostname, authenticated?" — it's "do you own this only in controlled server-side logic. then validate the resolved IP — don't trust ticket?" the pre-resolution name.
  12. A01 — Cross-Framework Laravel Symfony WordPress Policies / Gates on

    every resource action Voters + #[IsGranted] attribute current_user_can() on every privileged $fillable on every model DTOs from forms, not entities directly action Validated request objects — never raw Explicit property mapping — no mass wp_safe_remote_get() for outbound $request->all() hydration from request fetches — SSRF-hardened by design Nonces on every state-changing form
  13. OWASP TOP 10 · 2025 A05: Injection Untrusted input crossing

    into an interpreter — SQL, HTML/JS (XSS), shell. One of the oldest categories on the list and still devastatingly common. Rampart has two live examples: a raw SQL string interpolation and an unescaped Blade output directive.
  14. A05 — Brief The Core Problem Rampart's Vulnerabilities Untrusted input

    crosses into an interpreter: SQL, HTML/JS (XSS), shell. SQLi — ticket search uses raw string interpolation. User input goes The interpreter can't tell data from instructions — because you didn't directly into the query string. separate them. Stored XSS — ticket replies use {!! !!} (unescaped Blade output). Whatever the user types is rendered as HTML in the agent's browser.
  15. A05 — Exploit 1 SQL Injection In the search box,

    enter: ' OR 1=1 -- - → every ticket in the database 2 Stored XSS Reply to a ticket with a <script> tag that beacons the agent's is returned. Then use UNION SELECT to pull the users table — session cookie to an attacker-controlled server. Watch it fire when including password hashes. the agent opens the ticket. The agent's session is now yours.
  16. A05 — Your Turn (6 min) The Challenge Regression Tests

    to Write Parameterize the search query The SQLi payload returns only matching tickets — not the whole table Escape the reply output The XSS payload renders as inert text in the browser — not as an executed script Both tests should fail on the unfixed code and pass on yours.
  17. A05 — The Fix SQL Injection XSS Use parameterized queries

    or Eloquent bindings. Never interpolate Default-escape all output. In Blade, {{ }} escapes HTML entities input into SQL. The database driver handles escaping when you use automatically. Reserve {!! !!} only for content you generated — never placeholders — you don't have to think about it, and you can't forget it. for user-supplied content. Wrong: "SELECT * FROM tickets WHERE title = '$input'" Add a Content-Security-Policy header as a second layer of defense Right: DB::select('... WHERE title = ?', [$input]) — it limits what scripts can execute even if something slips through.
  18. A05 — Cross-Framework Laravel Symfony WordPress Query bindings / Eloquent

    — Doctrine parameterized queries $wpdb->prepare() for all custom queries parameterized by default Twig auto-escapes output by default esc_html() / esc_attr() / wp_kses() on Blade auto-escapes {{ }} Use |raw filter only for trusted content output Add CSP via middleware Never echo raw $_GET / $_POST
  19. OWASP TOP 10 · 2025 A07: Authentication Failures Renamed in

    2025 (was "Identification and Authentication Failures"). When authentication breaks, every other control collapses — an attacker who can become any user doesn't need to find a vulnerability in your business logic.
  20. A07 — Brief Renamed in 2025 Rampart's Vulnerabilities Previously "Identification

    and Authentication Failures." The rename Session fixation — no session regeneration on login emphasizes that the failure is in the authentication mechanism itself — Forgeable "remember me" cookie — it's just base64(user_id) not just identity management. No MFA offered Password reset tokens that don't expire
  21. A07 — Exploit 1 Session Fixation 2 Forged Remember Cookie

    Note the rampart-session cookie before logging in. Log in. The Decode the remember-me cookie — it's just base64 of a user ID. cookie value is unchanged. The server never rotates the session on Re-encode with 1 (the first admin's ID). Set the cookie. Reload. You login, so a session id fixed in the victim's browser stays valid and are logged in as the first admin. No password required. becomes authenticated.
  22. A07 — Your Turn (6 min) The Challenge What to

    Verify Regenerate the session on login After login, the session ID is different from the pre-login ID Sign or encrypt the persistent cookie so it can't be forged A tampered remember-me cookie is rejected — not silently accepted Public test suite still passes
  23. A07 — The Fix Session Regeneration Persistent Auth Tokens Reset

    Tokens & MFA Regenerate the session ID on every Must be random, signed, server-stored, Reset tokens: high-entropy, expiring, single- privilege change — especially login. In and single-use-ish. Never derived from use. Once used, invalidate immediately. Laravel: session()->regenerate(). This user ID. Generate with a CSPRNG, store a Offer MFA — even TOTP is a massive invalidates any pre-set session ID an hash server-side, verify on use, rotate on improvement over password-only auth. attacker planted. use.
  24. A07 — Cross-Framework Laravel Symfony WordPress Built-in guards + session()->regenerate()

    Security component handles session wp_set_auth_cookie() is HMAC-signed — Fortify / Breeze for MFA support migration on login don't roll your own Don't roll your own remember-me logic Remember-me with signed tokens built in MFA via plugin (many good options) Use the security bundle — don't Never store auth state in a plain cookie reimplement you control
  25. OWASP TOP 10 · 2025 A04: Cryptographic Failures Not "we

    need more crypto" — usually "we used it wrong or not at all." Weak hashing, predictable tokens, and plaintext secrets are the most common forms. Rampart has all three.
  26. A04 — Brief The Real Problem Rampart's Vulnerabilities Not "we

    need more crypto" — usually "we used it wrong or not at all." The MD5 password hashes — crackable in seconds with a wordlist APIs exist. The failure is reaching for the wrong one, or skipping it Plaintext API tokens in the database — readable by anyone with DB entirely. access Predictable reset token — md5(email . time()) — forgeable if you know the email and can guess the second No TLS enforcement
  27. A04 — Exploit (demo) 1 Crack the Hashes 2 Forge

    a Reset Token Dump the users table (you already can, via the A05 SQLi). The The reset token is md5(email . time()). You know the email. You can hashes are MD5. Run them against a wordlist — weak passwords guess the Unix timestamp to within a few seconds. Brute-force the crack in seconds. Common passwords crack in minutes. second → forge a valid password reset link → take over the account.
  28. A04 — Your Turn (5 min) The Challenge Bonus Swap

    password hashing to bcrypt or argon2 via the framework's Replace the reset token generation with random_bytes() Hash API Add an expiry to reset tokens Encrypt API tokens at rest — don't store them plaintext Write a regression test: a token generated more than N minutes ago is rejected
  29. A04 — The Fix Passwords Secrets at Rest Tokens &

    Transport Use password_hash() or the framework's Encrypt sensitive values (API tokens, keys) Generate tokens with a CSPRNG Hash facade (bcrypt or argon2id). Never using the framework encrypter or a KMS. (random_bytes()) — never time- or ID- MD5 or SHA1 for passwords. The API Don't log them. Don't store them plaintext in derived. Enforce TLS everywhere. Add handles salting automatically — you don't the database. HSTS so browsers remember. need to think about it.
  30. A04 — Cross-Framework Laravel Symfony WordPress Hash::make() — bcrypt or

    argon2 PasswordHasher component wp_hash_password() / Crypt::encrypt() for secrets at rest Secrets vault for environment secrets wp_check_password() — bcrypt as of 6.8 Str::random() backed by CSPRNG Don't store raw tokens — hash them Stop using raw MD5 — note that WP before storage itself moved off it Use application passwords for API access, not raw tokens
  31. OWASP TOP 10 · 2025 A02: Security Misconfiguration #5 →

    #2 in 2025 Jumped three spots to #2. Configuration now drives more application behavior than ever, and continuous delivery ships misconfiguration to production faster than any previous deployment model.
  32. A02 — Brief Why It Jumped to #2 Rampart's Vulnerabilities

    Config now drives more app behavior than ever, and CD ships misconfig APP_DEBUG=true in "prod" — stack traces leak env vars and DB fast. A single wrong environment variable can expose your entire credentials database schema, credentials, and encryption key in a single error page. Default admin login (admin@rampart / admin) Permissive CORS — any origin accepted Missing security headers Verbose errors exposed to end users
  33. A02 — Exploit (demo) 1 Debug Page Credential Leak 2

    Default Credentials Trigger a genuine server error (a 500, not a 404). Logged in with a Navigate to the admin login. Enter [email protected] / admin. bad search , anything. The debug page prints the full environment: You're in. No brute force required. The credentials were never DB credentials, app key, API secrets. Everything needed to own the changed from the defaults. application is on one page.
  34. A02 — Your Turn (5 min) The Challenge What to

    Verify Turn off debug mode for prod An error page shows a generic message — not a stack trace Remove or change the default credentials The default admin credentials are rejected Add the security-header middleware Security headers appear in the response (X-Frame-Options, ContentSecurity-Policy, Strict-Transport-Security)
  35. A02 — The Fix Debug & Secrets Credentials & Permissions

    Headers & CORS APP_DEBUG=false in prod. Environment- No default or shared credentials. Least- Security headers: CSP, HSTS, X-Frame- specific config. Secrets out of the repo — privilege DB user — the app user should not Options, X-Content-Type-Options. Lock use environment variables or a secrets have DROP TABLE rights. Force credential CORS to known origins — not *. These are manager, never committed values. rotation on first login for any seeded one-time config changes with significant accounts. impact.
  36. A02 — Cross-Framework Laravel Symfony WordPress APP_DEBUG=false, config:cache in prod

    APP_ENV=prod WP_DEBUG=false, DISALLOW_FILE_EDIT Header middleware for security headers Disable profiler and web-debug-toolbar in Move wp-config.php secrets to Don't ship Telescope or Ignition to prod prod environment variables Use secrets component for sensitive Security-header plugin or server config values
  37. OWASP TOP 10 · 2025 A06: Insecure Design The flaw

    is in the plan, not the code. You can't patch your way out of a bad design. This category exists because some vulnerabilities are architectural — they require rethinking the feature, not just fixing a line of code.
  38. A06 — Brief The Core Insight Rampart's Vulnerabilities The flaw

    is in the plan, not the code. You can't patch your way out of a No rate limiting or lockout on login & reset — brute force is trivially bad design. Security must be designed in — it cannot be bolted on after easy the fact. User enumeration — the reset endpoint returns different responses for known vs unknown emails Authorization decisions trusted from the client — the server doesn't verify
  39. A06 — Exploit (demo + discuss) 1 Brute Force Login

    2 User Enumeration via Reset → "Reset link sent." Hit it Script 500 login attempts against any account. Nothing stops you — Hit the reset endpoint with a known email no lockout, no throttle, no CAPTCHA. Given enough time and a with an unknown email wordlist, any account with a weak password falls. You can now enumerate every valid account in the system. → "Email not found." The responses differ.
  40. A06 — Your Turn (patch the enumeration) The Challenge What

    to Verify Make the reset response identical for known and unknown emails The response body, status code, and timing are identical regardless of Add throttling to the reset endpoint whether the email exists After N attempts, the endpoint slows down or stops responding Legitimate users can still reset their password
  41. A06 — The Fix Rate Limiting & Lockout Uniform Responses

    Design-Time Security Rate limiting + progressive lockout + Never leak the existence of an account. Decide trust boundaries in design. Write CAPTCHA on abuse-prone flows. The The reset response must be identical — abuse cases next to user stories. Every design must assume adversarial use from same body, same status code, same timing feature spec should have a "how could this the start — not add it as an afterthought — whether or not the email is registered. be abused?" section before a line of code is when abuse is detected in production. Use a constant-time response path. written.
  42. A06 — Cross-Framework Laravel Symfony WordPress RateLimiter / throttle middleware

    rate_limiter component Throttle at the edge or via plugin Framework password-reset already Login throttling built into the security Be careful — core wp_login errors do returns generic responses — don't undo component leak account validity, so harden it that Configure it — it's not on by default explicitly Use the built-in abstractions; they were This is a known WordPress design designed with these attacks in mind limitation
  43. OWASP TOP 10 · 2025 A03: Software Supply Chain Failures

    New framing for 2025 Expands the old "Vulnerable & Outdated Components" to the whole ecosystem: dependencies, build pipeline, and distribution. Your code is only as secure as everything it depends on.
  44. A03 — Brief The Expanded Scope Rampart's Vulnerabilities Old framing:

    "Vulnerable & Outdated Components." New framing: the A pinned dependency with a known CVE — composer audit findings whole ecosystem — dependencies, build pipeline, and distribution. An ignored attacker who compromises your build process owns everything you ship. A frontend script pulled from a CDN with no Subresource Integrity hash An unpinned CI action — pinned to a mutable tag, not a SHA
  45. A03 — Exploit / Audit 1 composer audit Lights Up

    2 CDN Script Without SRI 3 Unpinned CI Action Run composer audit. It reports known The <script src> tag has no integrity The CI workflow pins an action to a CVEs in pinned dependencies. We ignored hash. A compromised CDN — or a CDN mutable tag (e.g., @v3), not a SHA. A it. Production ignores it too. The that serves different content to different compromised action maintainer can push vulnerability has been there for months. regions — owns every page that loads it. malicious code to that tag and it runs in No warning. No error. your pipeline on the next build.
  46. A03 — Your Turn (quick) Three Tasks Update the flagged

    dependency to a non-vulnerable version Add an integrity attribute (SRI hash) to the CDN <script> tag Pin the CI action to a full commit SHA instead of a mutable tag How to Generate an SRI Hash Use the SRI Hash Generator at srihash.org, or run: curl -s [url] | openssl dgst -sha384 -binary | openssl base64 -A Then add: integrity="sha384-[hash]" crossorigin="anonymous"
  47. A03 — The Fix Audit in CI Pin Everything by

    Hash Own Your Build Integrity composer audit / npm audit in CI, failing Pin dependencies and CI actions by hash, Own your build & distribution integrity, not the build on known-vulnerable not by mutable tag. Verify lockfiles are just your source. The attack surface dependencies. If it doesn't break the build, committed and used in production installs. includes your CI runner, your artifact store, it gets ignored. Make it impossible to ship a SRI on all third-party scripts — no and your deployment pipeline — not just known-vulnerable dep. exceptions. your src/ directory.
  48. A03 — Cross-Framework Laravel / Symfony (Composer) WordPress composer audit

    in CI — fail on high/critical Plugins and themes are your supply chain Dependabot or Renovate for automated PRs Enable auto-updates for security releases --locked installs in production — never composer update in prod Remove abandoned plugins — no maintainer means no patches Commit and verify composer.lock Watch the WordPress plugin repo advisories feed
  49. OWASP TOP 10 · 2025 A08: Software or Data Integrity

    Failures Trusting data or code you didn't verify. Home of insecure deserialization. When your application blindramply trusts serialized data from users, it hands them a mechanism to inject arbitrary objects — and potentially execute arbitrary code.
  50. A08 — Brief The Core Problem Rampart's Vulnerabilities Trusting data

    or code you didn't verify. Home of insecure deserialization. Saved "views"/preferences stored as PHP serialize() and unserialize()- PHP's unserialize() is particularly dangerous — it instantiates objects, ed with no allowed_classes which can trigger magic methods and POP gadget chains. A webhook receiver that runs unsigned payloads — no signature verification → object injection
  51. A08 — Exploit 1 Object Injection via Deserialization 2 Unsigned

    Webhook Your "saved view" preference round-trips through unserialize(). Craft Forge a webhook call — there's no signature check, so the app trusts a serialized object that triggers a POP gadget (or, minimally, it. Send any payload you want. The app processes it as if it came tampers with app state) from a legitimate source. → code or logic execution. The app instantiates whatever class you name in the serialized string.
  52. A08 — Your Turn (patch) Deserialization Fix Webhook Fix Replace

    serialize() / unserialize() with JSON for stored preferences. Add HMAC signature verification to the webhook receiver. The sender If you must use unserialize(), pass ['allowed_classes' => false] as the second argument — this prevents object instantiation entirely. signs the payload with a shared secret. The receiver verifies the signature before processing anything. Use hash_equals() for the comparison — not == — to prevent timing attacks.
  53. A08 — The Fix Don't Unserialize Untrusted Data Verify Integrity

    at Every Trust Boundary Use JSON for all stored state that crosses a trust boundary. JSON Sign webhooks (HMAC) and verify before processing. Verify software cannot instantiate objects. If unserialize() is unavoidable, restrict updates before applying. Check artifact signatures in your build allowed classes to an explicit allowlist — or pass false to block all pipeline. Anything crossing a trust boundary must be verified — not class instantiation. assumed.
  54. A08 — Cross-Framework Laravel Symfony WordPress Signed URLs for integrity-sensitive

    links Avoid PHP serialization of untrusted maybe_unserialize() on trusted DB data Cookie encryption and signing built in input only — never on request input JSON for all stored state — avoid PHP Use the Serializer component with Verify webhook signatures before serialization entirely explicit formats (JSON, XML) processing Explicit type mapping — no magic class Use the REST API with nonce/OAuth — instantiation don't build custom webhook receivers without signing
  55. OWASP TOP 10 · 2025 · BRAND NEW A10: Mishandling

    of Exceptional Conditions A completely new category in 2025. Most of the room has never seen it on the list. The failure: the app doesn't prevent, doesn't detect, or responds badly to abnormal conditions — classically, it fails open instead of failing closed.
  56. A10 — Brief Brand New in 2025 Rampart's Vulnerabilities Completely

    new category. Most of the room has never seen it on the list. An authorization guard wrapped in try/catch that returns true The failure: the app doesn't prevent, doesn't detect, or responds badly to (authorized!) on any exception abnormal conditions. A login lockout that stops working when the cache is down — silently Classically: it fails open instead of failing closed. fails open, restoring unlimited login attempts
  57. A10 — Exploit (demo) 1 Exception → Authorization Bypass 2

    Cache Down → Lockout Disabled Send input that makes the permission check throw — a malformed Take the cache offline. The login lockout counter lived in the cache. ID, an oversized value, anything that triggers an exception. The It silently fails open. Unlimited login attempts return. The brute-force catch block "helpfully" defaults to allow protection you thought you had is gone — and there's no alert telling → you're authorized. The guard that was supposed to protect the resource is now the door opener. you it's gone.
  58. A10 — Your Turn (patch) The Challenge Regression Test to

    Write Make the exceptional path deny, not allow Assert that an exception thrown inside the authorization guard results in Handle the dependency-down case as "fail closed" — if the cache is access denied — not access granted. unavailable, deny the login attempt rather than skipping the check This test should fail on the current code (exception your fix (exception → deny). → allow) and pass on
  59. A10 — The Fix Fail Closed Catch Narrowly, Handle Deliberately

    Design for Dependency Failures allow/continue. If a security check cannot Never swallow-and-continue on a security cache, DB, external service. What happens complete successfully, the answer is no. decision. Catch specific exception types. to your security controls when Redis is This is the fundamental principle: safe Handle them explicitly. Log them. Don't use unavailable? When the auth service times defaults mean the secure state is the a broad catch (\Throwable $e) that silently out? The answer must be: deny, not allow. fallback state. continues past a failed security check. The default on error is deny/stop — never Design for the dependency being down —
  60. A10 — Cross-Framework Framework-Agnostic Principle The safe default when a

    check can't complete is to deny. This applies identically in Laravel, Symfony, and WordPress. No framework makes this decision for you — it's a design choice you make in every guard, every middleware, every permission check. Practical Checklist Every try/catch around a security check: what does the catch return? Every external dependency used in auth/authz: what happens when it's down? Every rate limiter / lockout: does it fail open or closed when its backing store is unavailable? Add a test for each: exception → deny, dependency down → deny
  61. OWASP TOP 10 · 2025 A09: Security Logging & Alerting

    Failures Renamed in 2025 to stress alerting — great logs with no alerts are almost worthless. You can't respond to a breach you don't know about. And logging done wrong creates its own exposure.
  62. A09 — Brief Renamed in 2025 Rampart's Dual Sin The

    rename stresses alerting. Great logs with no alerts are almost No audit trail on logins, admin actions, or any of the exploits you just worthless. Detection without response is theater. You need both — and ran you need to know what to alert on. No alerting — you'd never know you were breached And the opposite problem: full requests (including passwords) written to laravel.log Logging done wrong is its own exposure. The log file is now a credential store.
  63. A09 — Observe the Absence 1 Re-Run Any Attack. Check

    the Logs. 2 Open laravel.log Re-run any attack from today. Now open the logs. Nothing security- There are user passwords in cleartext. The framework logged the relevant is there. No failed login. No privilege escalation. No IDOR. full request — including the password field — during a validation No SQLi. You'd never know you were breached. The attacker has error. Logging done wrong is its own exposure. The log file is now a been in for hours and the logs are clean. credential store.
  64. A09 — Your Turn (quick) The Challenge What Good Looks

    Like Add an audit log for auth events (login, logout, failed login) Every login attempt (success and failure) is logged with timestamp, IP, Add an audit log for admin events (user changes, role changes) and user identifier Redact secrets and passwords from all log output Every admin action is logged with who did what to whom No passwords, tokens, or secrets appear anywhere in the log files An alert fires when N failed logins occur in M minutes
  65. A09 — The Fix Log Security Events Alert on What

    Matters Redact & Protect Log authentication events, authorization Alert on the events that matter — repeated Redact secrets and PII from logs. Treat log failures, and admin actions with enough failed logins, privilege escalations, access files as sensitive data — restrict access, context to investigate — who, what, when, to sensitive resources. Logs without alerts rotate them, ship them to a SIEM. Monitor from where. Without this, you cannot are archaeology. You need to know in real for the attacks you can't prevent. The log is detect a breach, let alone respond to one. time, not during the post-mortem. your last line of detection.
  66. A09 — Cross-Framework Laravel Symfony WordPress Structured logging channels (daily,

    stack, Monolog channels + processors for Audit-log plugin or custom do_action etc.) redaction hooks on security events Log message processors to redact Security event listeners for auth events Scrub secrets from debug.log sensitive fields Ship logs to a centralized store — not just WP_DEBUG_LOG=false in prod — or Audit packages (e.g., owen-it/laravel- local files redirect to a secure location auditing) Don't log request bodies in prod without redaction
  67. WRAP-UP Wrap You've broken and fixed all ten OWASP categories.

    Here's what you've built: a set of regression tests that prove your fixes work, a mental model for thinking adversarially about every feature you write, and a hardening checklist you can apply to production code starting Monday.
  68. The Hardening Checklist A01 A02 A03 Authorize the object, not

    just the session Secure defaults — debug off, no default Audit deps in CI — fail the build on known creds, security headers CVEs A04 A05 Use the crypto APIs — bcrypt, CSPRNG, encrypt at rest Bind + escape — parameterize SQL, escape output A06 A07 A08 Threat-model abuse cases — write them next Use framework auth — regenerate sessions, Don't trust unverified data — JSON over to user stories sign tokens serialize, sign webhooks A09 A10 Log + alert, redact secrets — detection is not optional Fail closed — the default on error is deny, never allow
  69. The Two Test Suites — The Reveal composer test —

    The Public Suite composer test:exploits — The Hidden Suite All day you ran composer test. It proves the app works, and it's yours to There's a second suite: composer test:exploits. It asserts that every bug extend with the regression tests you wrote today. Every bug you closed you hunted exists exactly as designed. It's how the app stays reliably should have a test that fails before the fix and passes after. broken between conferences. Run it now: green means all the vulns are present. As you fix them on a branch, watch it go red — that's your fixes landing. Red on the exploit suite + green on the public suite = you're done.
  70. Take It Home Repo + Lab Guide + Hardening Checklist

    Keep Learning The One Thing to Remember PHP Architect Security Corner — if you The exploits are cheap. Anyone can break github.com/ericmann/rampart want this monthly. Eric covers PHP security it. The fixes take real work. Write the topics, new vulnerabilities, and practical regression test. Ship the patch. Make it hardening techniques in every issue. impossible to regress. That's what Fork it, break it worse, use it to train your team. The exploit suite keeps it reliably broken so it's always ready for the next workshop. separates a security-aware developer from everyone else.