Horizon Security Analyzer
| Analyzer ID | Category | Severity | Time To Fix |
|---|---|---|---|
horizon-security | 🛡️ Security | Critical | 10 minutes |
What This Checks
This analyzer validates the security configuration of Laravel Horizon's dashboard to ensure it is not publicly accessible. Horizon provides a web-based dashboard for monitoring Redis queues, failed jobs, and job metrics -- sensitive operational data that must be protected.
Checks Performed:
Service Provider Validation
The gate is read from the parsed syntax tree, so it is found in any file under app/Providers or in bootstrap/app.php, not only in HorizonServiceProvider.
- HorizonServiceProvider existence - Verifies
app/Providers/HorizonServiceProvider.phpexists - Authorization gate - Checks that
Gate::define('viewHorizon', ...)orHorizon::auth(...)is registered - Blanket grant - Flags
return true;andfn ($user) => true, plus callbacks that never read their argument - Auth-only checks - Flags gates that only check whether anyone is signed in:
auth()->check(),Auth::check(),$request->user() !== null - Permissive fallback - Flags
?? trueor?: truein the fallback position - Environment bypass - Flags an environment or debug check beyond
localthat can grant on its own - Auth middleware - Checks for authentication middleware on the Horizon routes
Configuration File Validation
- Middleware configuration - Flags when
config/horizon.phponly includeswebmiddleware withoutauth, but only when noviewHorizongate is configured. When a gate is present it already handles authorization, makingauthmiddleware redundant.
Redis Configuration Validation
- Redis password - Flags when
config/database.phphas a hardcodednullor empty password for the Redis connection instead of usingenv('REDIS_PASSWORD')
Gate Severity Is Graded by Reach
The gate defects above are not reported at a fixed level. Where the gate is registered decides the grade:
- Unconditionally - High, or Critical for a blanket grant
- Only inside an
environment('local')check - Low, or Medium for a blanket grant, since Laravel's own dashboards already behave that way by default - Behind any wider guard, such as
staging- between the two
Why It Matters
An unsecured Horizon dashboard exposes critical operational information that can be leveraged by attackers:
- Sensitive Job Data - Job payloads may contain user emails, payment data, API tokens, and other PII
- Application Architecture - Queue names, job classes, and processing patterns reveal internal architecture
- Failed Job Information - Failed job details often include stack traces, database queries, and configuration values
- Queue Metrics - Throughput and timing data can inform denial-of-service attack strategies
- Redis Configuration - Connection details and memory usage patterns can be exploited
A publicly accessible Horizon dashboard is equivalent to giving an attacker a monitoring console for your application's background processing.
How to Fix
Quick Fix (5 minutes)
Add a viewHorizon gate in your HorizonServiceProvider. A role-check or email-allowlist is sufficient — no environment-specific branching is required.
Before (❌):
// app/Providers/HorizonServiceProvider.php
class HorizonServiceProvider extends HorizonApplicationServiceProvider
{
protected function gate(): void
{
// NO AUTHORIZATION - access falls back to "local environment only"
}
}After (✅):
// app/Providers/HorizonServiceProvider.php
use Illuminate\Support\Facades\Gate;
class HorizonServiceProvider extends HorizonApplicationServiceProvider
{
protected function gate(): void
{
Gate::define('viewHorizon', function ($user): bool {
return $user->hasRole('admin');
});
}
}Alternatively, drive the allowlist from an environment variable so no code change is needed per environment:
protected function gate(): void
{
Gate::define('viewHorizon', function ($user): bool {
$authorized = array_filter(
array_map('trim', explode(',', config('horizon.authorized_emails', '')))
);
return in_array($user->email, $authorized, strict: true);
});
}# .env
HORIZON_AUTHORIZED_EMAILS=admin@yourcompany.com,devops@yourcompany.comProper Fix (10 minutes)
Also make sure weak gates are not in use:
Before (❌) — grants access to everyone:
protected function gate(): void
{
Horizon::auth(function ($request) {
return true; // VULNERABLE: no real authorization
});
}Before (❌) — authentication only, not authorization:
protected function gate(): void
{
Horizon::auth(function ($request) {
return auth()->check(); // Any logged-in user can access Horizon
});
}Before (❌) — permissive fallback:
protected function gate(): void
{
Gate::define('viewHorizon', function ($user): bool {
// VULNERABLE: opens the dashboard whenever the real check cannot be
// answered, such as a null user or a missing relation
return $user?->isAdmin() ?? true;
});
}Before (❌) — environment bypass:
protected function gate(): void
{
Gate::define('viewHorizon', function ($user): bool {
// VULNERABLE: access now depends on APP_ENV staying correct on every
// deployed box, which is configuration rather than authorization
return app()->environment('staging') || $user->hasRole('admin');
});
}After (✅) — proper role/permission check:
// app/Providers/HorizonServiceProvider.php
use Illuminate\Support\Facades\Gate;
class HorizonServiceProvider extends HorizonApplicationServiceProvider
{
protected function gate(): void
{
Gate::define('viewHorizon', function ($user): bool {
return $user->hasRole('admin')
|| $user->can('view-horizon');
});
}
}Optional: add auth middleware as an extra layer (not required when a gate is configured):
// config/horizon.php
return [
'middleware' => ['web', 'auth'], // 'auth' ensures unauthenticated users get a login redirect
// ...
];Fix Redis Authentication
Ensure the Redis password is read from environment variables, not hardcoded:
Before (❌):
// config/database.php
'redis' => [
'default' => [
'host' => env('REDIS_HOST', '127.0.0.1'),
'password' => null, // Unauthenticated Redis
'port' => env('REDIS_PORT', 6379),
],
],After (✅):
// config/database.php
'redis' => [
'default' => [
'host' => env('REDIS_HOST', '127.0.0.1'),
'password' => env('REDIS_PASSWORD'),
'port' => env('REDIS_PORT', 6379),
],
],# .env
REDIS_PASSWORD=your-strong-redis-passwordReferences
- Laravel Horizon Documentation
- Laravel Horizon Authorization
- OWASP Access Control Cheat Sheet
- CWE-284: Improper Access Control
- CWE-306: Missing Authentication for Critical Function
Related Analyzers
- Nova Security Analyzer - Validates Laravel Nova admin panel security
- Authentication Authorization Analyzer - Checks authentication and authorization configuration
- Debug Mode Analyzer - Ensures debug mode is disabled in production
- Env HTTP Accessibility Analyzer - Prevents sensitive files from being publicly accessible