OWASP Top 10
The ten most critical web application security risks — 2021 edition. Every fullstack engineer should know them cold.
Broken Access Control
Users act outside their intended permissions — accessing other users' data, admin pages, or performing privileged operations without authorization.
Vulnerablepublic function show(int $id): Response { // No check: any logged-in user can see any order! $order = Order::findOrFail($id); return response()->json($order); }
Securepublic function show(int $id): Response { $order = Order::findOrFail($id); // Verify caller owns this resource if ($order->user_id !== auth()->id()) { abort(403); } return response()->json($order); }
- Enforce authorization on every endpoint — never trust the client
- Use resource-level checks, not just role checks (own vs all)
- Default to deny: explicit allow-listing over block-listing
- Use Laravel Policies / Gates for consistent authorization logic
Cryptographic Failures
Sensitive data exposed due to weak or absent encryption — in transit, at rest, or in logs. Formerly called "Sensitive Data Exposure".
Vulnerable// Storing passwords with MD5 $hash = md5($password); // ⚠ broken // Logging sensitive data Log::info('Login: ' . $email . ' ' . $password); // Transmitting over HTTP (no TLS) 'APP_URL=http://myapp.com'
Secure// Use bcrypt / Argon2 (Laravel default) $hash = Hash::make($password); // ✓ // Never log credentials or tokens Log::info('Login attempt: ' . $email); // Enforce HTTPS everywhere 'APP_URL=https://myapp.com' URL::forceScheme('https');
- Use HTTPS for all traffic — no mixed content
- Hash passwords with bcrypt, Argon2id, or scrypt
- Encrypt sensitive fields at rest (credit cards, SSNs)
- Never log passwords, tokens, or PII
Injection
User-supplied data is sent to an interpreter without validation or escaping — SQL, OS, LDAP, NoSQL injection. An attacker can read, modify, or delete data.
Vulnerable — SQL Injection$query = "SELECT * FROM users WHERE email = '" . $request->email . "'"; // ⚠ DB::select($query); // Input: ' OR '1'='1 // Query becomes: WHERE email='' OR '1'='1' // → returns ALL users
Secure — Parameterized Query// Use Eloquent (parameterized by default) User::where('email', $request->email)->first(); // Or raw with binding DB::select( 'SELECT * FROM users WHERE email = ?', [$request->email] // ✓ safely bound );
- Always use parameterized queries or an ORM
- Validate and whitelist user input at the boundary
- Apply least privilege to DB accounts (no DROP/TRUNCATE)
- For HTML output use context-aware escaping (Blade {{ }} auto-escapes)
Insecure Design
Security risks baked into the architecture — not caused by bugs but by missing or flawed design decisions. Cannot be fixed by implementation alone.
Common Insecure Design Patterns
- Password reset via predictable tokens (user ID + timestamp)
- Unlimited API retries — no rate limiting designed in
- Business logic that trusts client-side state
- No separation between admin and user data paths
- Multi-tenancy without data isolation at the DB layer
Secure Design Practices
- Threat modeling: identify threats before writing code
- Establish security requirements alongside functional ones
- Design for principle of least privilege from the start
- Use secure design patterns (separate read/write paths)
- Limit resource consumption by design (quotas, pagination)
Rule: Security is a design activity, not a testing activity. A secure implementation of an insecure design is still insecure. Start with threat modeling.
Security Misconfiguration
The application or infrastructure is configured insecurely — debug mode on, default credentials, overly permissive CORS, verbose error messages.
Vulnerable — production .envAPP_ENV=local // ⚠ wrong APP_DEBUG=true // ⚠ exposes stack traces DB_PASSWORD= // ⚠ empty password TELESCOPE_ENABLED=true // ⚠ debug UI public // XML external entity processing enabled libxml_disable_entity_loader(false);
Secure — production .envAPP_ENV=production APP_DEBUG=false DB_PASSWORD=<strong-secret> TELESCOPE_ENABLED=false // Restrict CORS to known origins 'allowed_origins' => ['https://myapp.com'], // Set security headers 'X-Frame-Options': DENY
- Automate environment configuration with Infrastructure as Code
- Disable all default accounts and sample applications
- Set security headers (CSP, X-Frame-Options, HSTS)
- Run
php artisan config:clearand audit with security scanners
Vulnerable and Outdated Components
Using libraries, frameworks, or other software with known vulnerabilities. The attacker targets the dependency, not your code.
Risk// Never checking for vulnerabilities $ composer update --no-scripts // blindly update // Using abandoned packages "league/oauth2-server": "^4.0" // 2018, EOL // No lock file committed (unpredictable deps) .gitignore: composer.lock // ⚠
Secure// Audit regularly $ composer audit // Pin versions in production $ composer install --no-dev // use lock file ✓ // Monitor CVEs with Dependabot / Renovate // Subscribe to security advisories // github.com/FriendsOfPHP/security-advisories
- Run
composer auditandnpm auditin CI on every build - Always commit and use
composer.lock/package-lock.json - Remove unused dependencies — every package is an attack surface
- Subscribe to CVE alerts for your key dependencies
Identification & Authentication Failures
Broken authentication — weak passwords, no MFA, insecure session management, or missing rate limits that allow credential stuffing.
Vulnerablepublic function login(Request $request): Response { // No rate limiting — allows brute-force $user = User::where('email', $request->email) ->first(); if (Hash::check($request->password, $user->password)) { Auth::login($user); return redirect('/'); } }
Secureuse Illuminate\Routing\Middleware\ThrottleRequests; Route::post('/login', [AuthController::class, 'login']) ->middleware('throttle:5,1'); // 5/min ✓ // session.php — secure session config 'secure' => true, // HTTPS only 'http_only' => true, // no JS access 'same_site' => 'strict', // CSRF protection
- Rate-limit login, password reset, and OTP endpoints
- Enforce strong passwords and offer MFA
- Invalidate sessions on logout and password change
- Use secure, HttpOnly, SameSite=Strict cookies
Software & Data Integrity Failures
Code and infrastructure that relies on plugins, libraries, or data from untrusted sources without integrity verification. Includes insecure CI/CD pipelines.
Vulnerable// Deserializing untrusted user input $data = unserialize($request->data); // ⚠ // Loading script with no integrity check<script src="https://cdn.example.com/lib.js"> </script> // ⚠ supply chain risk // Auto-deploying from any pull request // (no review, no signing)
Secure// Use JSON, not serialize $data = json_decode($request->data, true); // ✓ // Subresource Integrity (SRI) for CDN scripts<script src="https://cdn.example.com/lib.js" integrity="sha384-abc123..." crossorigin="anonymous"></script> // Require signed commits in CI/CD
- Never deserialize untrusted data with PHP's
unserialize() - Use Subresource Integrity (SRI) for all external scripts and styles
- Protect CI/CD pipelines — require code review before deploy
- Sign build artifacts and verify signatures on deploy
Security Logging & Monitoring Failures
Without adequate logging and monitoring, breaches go undetected. The average time to detect a breach is over 200 days.
Vulnerablepublic function login(Request $request): Response { if (Auth::attempt($request->only('email', 'password'))) { return redirect('/'); // No log of successful login } return back()->withErrors([...]); // No log of failed attempt either }
Secureif (Auth::attempt($credentials)) { Log::channel('security')->info('Login success', [ 'user' => $request->email, 'ip' => $request->ip(), 'ua' => $request->userAgent(), ]); } else { Log::channel('security')->warning('Login failed', [ 'email' => $request->email, 'ip' => $request->ip(), ]); }
- Log all authentication events (success, failure, logout)
- Log authorization failures and privilege escalation attempts
- Store logs in tamper-proof storage, separate from the app
- Set up alerts for anomalies (many failures from one IP, after-hours access)
Server-Side Request Forgery (SSRF)
The server fetches a URL supplied by the user — the attacker redirects requests to internal services, metadata endpoints, or the local filesystem.
Server-side URL validationfunction fetchWebhookContent(string $url): string { $parsed = parse_url($url); // Allowlist of permitted hosts $allowed = ['api.partner.com', 'hooks.service.io']; if (!in_array($parsed['host'], $allowed)) { throw new InvalidArgumentException(); } // Also block RFC-1918 / loopback ranges // 10.x, 192.168.x, 172.16.x, 127.x return Http::get($url)->body(); }
- Validate URLs against a strict allowlist of hosts and protocols
- Block requests to RFC-1918 (private), loopback, and metadata addresses
- Do not forward raw HTTP responses to the client
- Disable HTTP redirects in outgoing HTTP clients when possible