How We Audited a High-Traffic Laravel Enterprise Stack on OVH and Mitigated mass assignment vulnerabilities in custom checkout models
Initial Stack Assessment and Audit Strategy
Our engagement began with a deep dive into a high-traffic Laravel enterprise application hosted on OVH’s dedicated server infrastructure. The primary objective was to identify and remediate security vulnerabilities, with a specific focus on mass assignment flaws within custom checkout models, a common attack vector in applications handling sensitive financial data. The stack comprised a typical Laravel setup: Nginx as the web server, PHP-FPM for execution, MySQL for the database, and Redis for caching and session management. The OVH environment provided dedicated bare-metal servers, offering granular control but also placing the full burden of security patching and configuration on our team.
Our audit strategy was multi-pronged:
- Code Review: Focused on areas handling user input, particularly during the checkout process, and identified potential mass assignment risks.
- Configuration Audit: Examined Nginx, PHP-FPM, and MySQL configurations for security best practices and potential misconfigurations.
- Runtime Analysis: Utilized application logs and system monitoring tools to detect anomalous behavior.
- Penetration Testing: Simulated common attack scenarios, including mass assignment exploits.
Identifying Mass Assignment Vulnerabilities in Custom Models
Mass assignment vulnerabilities arise when an application directly binds user-supplied input (e.g., from HTTP POST requests) to Eloquent model attributes without proper sanitization or explicit whitelisting. This can allow an attacker to modify attributes that should not be user-editable, such as `is_admin`, `price`, or `order_status`.
Consider a simplified `Order` model and its associated controller logic:
// app/Models/Order.php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class Order extends Model
{
// Potentially vulnerable if not explicitly guarded
protected $fillable = [
'user_id',
'product_id',
'quantity',
'shipping_address',
'billing_address',
// Missing 'total_price' and 'status' from fillable
];
// ... other model logic
}
// app/Http/Controllers/CheckoutController.php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use App\Models\Order;
class CheckoutController extends Controller
{
public function store(Request $request)
{
// Vulnerable: Directly mass-assigns all request data
$order = Order::create($request->all());
// ... further order processing
}
}
In the above example, if an attacker were to send a request with data like:
{
"user_id": 1,
"product_id": 10,
"quantity": 2,
"shipping_address": "123 Main St",
"billing_address": "123 Main St",
"total_price": 0.01, // Attacker attempts to set a low price
"status": "completed" // Attacker attempts to bypass payment validation
}
And if `total_price` and `status` were *not* explicitly listed in the `$fillable` array (or were present in a `$guarded` array that allowed them), Laravel’s `Order::create($request->all())` would attempt to create the order with these attacker-controlled values. This is particularly dangerous in custom checkout models where `total_price` and `status` are critical financial and state-management attributes.
Mitigation Strategy: Strict Fillable/Guarded Attributes and Input Validation
The primary defense against mass assignment is to be explicit about which attributes are allowed to be mass-assigned. Laravel provides two mechanisms for this: `$fillable` and `$guarded`.
1. Using `$fillable` (Whitelisting): This is the recommended approach for most scenarios. You explicitly list all attributes that *can* be mass-assigned. Any attribute not in this array will be ignored during mass assignment.
// app/Models/Order.php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class Order extends Model
{
// Explicitly list only the attributes that can be set via user input
protected $fillable = [
'user_id',
'product_id',
'quantity',
'shipping_address',
'billing_address',
// 'total_price' and 'status' are NOT included here.
];
// ...
}
With this change, the attacker’s attempt to set `total_price` and `status` would be silently ignored by Eloquent. The order would be created with default values for these fields, which would then be updated through secure, server-side logic (e.g., after payment processing).
2. Using `$guarded` (Blacklisting): This approach lists attributes that *cannot* be mass-assigned. If `$guarded` is an empty array (`[]`), then *all* attributes are guarded, effectively disabling mass assignment unless they are explicitly in `$fillable`. If `$guarded` contains specific attributes, those are protected.
// app/Models/Order.php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class Order extends Model
{
// Guard all attributes by default, then explicitly allow specific ones if needed
// Or, guard specific sensitive attributes.
protected $guarded = ['total_price', 'status', 'discount_code', 'is_paid'];
// If you use $guarded, you typically don't need $fillable,
// unless you want to be even more restrictive.
// If both are present, $fillable takes precedence.
// ...
}
3. Server-Side Validation: Even with strict `$fillable` or `$guarded` settings, robust server-side validation is crucial. This ensures data integrity and prevents unexpected behavior.
// app/Http/Controllers/CheckoutController.php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use App\Models\Order;
use Illuminate\Support\Facades\Validator;
class CheckoutController extends Controller
{
public function store(Request $request)
{
$validatedData = Validator::make($request->all(), [
'product_id' => 'required|exists:products,id',
'quantity' => 'required|integer|min:1',
'shipping_address' => 'required|string',
'billing_address' => 'required|string',
// Note: 'total_price' and 'status' are NOT validated here from user input.
// They will be calculated and set server-side.
])->validate();
// Create order with validated, safe data
$order = Order::create($validatedData);
// Server-side logic to calculate total_price based on product, quantity, and promotions
$order->total_price = $this->calculateTotalPrice($order);
$order->status = 'pending_payment'; // Default status
$order->save();
// ... further order processing, payment gateway integration
}
protected function calculateTotalPrice(Order $order): float
{
// Complex pricing logic, promotions, taxes, etc.
$product = \App\Models\Product::find($order->product_id);
$basePrice = $product->price * $order->quantity;
// Apply discounts, taxes, etc.
return $basePrice; // Simplified for example
}
}
This layered approach ensures that only explicitly allowed fields are ever considered for mass assignment, and all incoming data is rigorously validated before being used to create or update model instances.
OVH Infrastructure Configuration for Security
Beyond application-level security, the OVH infrastructure required specific hardening. Given the dedicated nature of the servers, we focused on:
1. Nginx Configuration:
# /etc/nginx/sites-available/your_app.conf
server {
listen 80;
listen [::]:80;
server_name your_domain.com www.your_domain.com;
return 301 https://$host$request_uri; # Redirect HTTP to HTTPS
}
server {
listen 443 ssl http2;
listen [::]:443 ssl http2;
server_name your_domain.com www.your_domain.com;
root /var/www/your_app/public;
index index.php index.html index.htm;
# SSL Configuration
ssl_certificate /etc/letsencrypt/live/your_domain.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/your_domain.com/privkey.pem;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_prefer_server_ciphers on;
ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384;
ssl_session_timeout 1d;
ssl_session_cache shared:SSL:10m;
ssl_session_tickets off;
# Security Headers
add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always;
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-XSS-Protection "1; mode=block" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
# add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline'; object-src 'none';" always; # CSP needs careful tuning
location / {
try_files $uri $uri/ /index.php?$query_string;
}
location ~ \.php$ {
include snippets/fastcgi-php.conf;
fastcgi_pass unix:/var/run/php/php8.1-fpm.sock; # Adjust PHP version as needed
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
include fastcgi_params;
# Rate Limiting (example: 100 requests per minute per IP)
limit_req_zone $binary_remote_addr zone=mylimit:10m rate=100r/min;
limit_req zone=mylimit burst=20 nodelay;
}
# Deny access to hidden files
location ~ /\. {
deny all;
}
# Deny access to sensitive files
location ~* (composer\.json|composer\.lock|\.env|\.git|\.env\.example) {
deny all;
}
}
2. PHP-FPM Configuration:
; /etc/php/8.1/fpm/php.ini (Adjust version as needed) ; Disable dangerous functions disable_functions = exec,passthru,shell_exec,system,proc_open,popen,curl_exec,curl_multi_exec,parse_ini_file,show_source,pcntl_exec,escapeshellarg,escapeshellcmd ; Set memory limits and execution times appropriately for your application memory_limit = 256M max_execution_time = 120 ; Error reporting for production (log errors, don't display them) display_errors = Off log_errors = On error_log = /var/log/php/php-fpm.log ; Session settings (if not using Redis for sessions) session.cookie_httponly = 1 session.cookie_secure = 1 session.use_strict_mode = 1
; /etc/php/8.1/fpm/pool.d/your_app.conf (Adjust version as needed) [your_app] user = www-data group = www-data listen = /var/run/php/php8.1-fpm.sock ; Security enhancements chroot = /var/www/your_app ; pm.max_children = 50 ; pm.start_servers = 5 ; pm.min_spare_servers = 2 ; pm.max_spare_servers = 10 ; pm.process_idle_timeout = 10s ; pm.max_requests = 500
3. MySQL Security:
-- Connect to MySQL as root -- mysql -u root -p -- Create a dedicated user for the application CREATE USER 'your_app_user'@'localhost' IDENTIFIED BY 'your_strong_password'; -- Grant minimal necessary privileges GRANT SELECT, INSERT, UPDATE, DELETE, INDEX, CREATE TEMPORARY TABLES ON your_database.* TO 'your_app_user'@'localhost'; -- Revoke all other privileges REVOKE ALL PRIVILEGES ON *.* FROM 'your_app_user'@'localhost'; -- Apply the changes FLUSH PRIVILEGES; -- Ensure sensitive data is encrypted at rest if necessary -- (This is often handled at the filesystem or disk level on OVH, or via application-level encryption)
4. Firewall and Network Security:
On OVH dedicated servers, `iptables` or `ufw` are essential. We configured strict inbound rules, allowing only necessary ports (SSH, HTTP, HTTPS) and blocking all others. Outbound traffic was also scrutinized.
# Example using ufw sudo ufw default deny incoming sudo ufw default allow outgoing sudo ufw allow ssh # Port 22 sudo ufw allow http # Port 80 sudo ufw allow https # Port 443 sudo ufw enable
Runtime Monitoring and Log Analysis
Continuous monitoring is key to detecting and responding to security incidents. We integrated:
1. Centralized Logging: Laravel’s Monolog was configured to send logs to a central ELK stack (Elasticsearch, Logstash, Kibana) for aggregation and analysis. This allowed us to correlate events across different servers and applications.
// config/logging.php
'channels' => [
// ... other channels
'elk' => [
'driver' => 'monolog',
'level' => env('LOG_LEVEL', 'debug'),
'handler' => \Monolog\Handler\ElasticsearchHandler::class,
'with' => [
'hosts' => [env('ELASTICSEARCH_HOSTS')],
'index' => env('ELASTICSEARCH_INDEX', 'laravel-logs'),
],
],
],
'default' => env('LOG_CHANNEL', 'stack'), // Change to 'elk' or 'daily' for production
2. Intrusion Detection Systems (IDS): Tools like Suricata or Snort were deployed on the network edge or host-level to detect malicious traffic patterns.
3. Application Performance Monitoring (APM): Tools like New Relic or Datadog provided insights into application behavior, helping to identify performance anomalies that could indicate an attack.
Conclusion and Ongoing Security Posture
By implementing strict `$fillable`/`$guarded` attributes, robust server-side validation, and hardening the OVH infrastructure with secure Nginx, PHP-FPM, and MySQL configurations, we significantly reduced the attack surface for mass assignment vulnerabilities. The integration of centralized logging and runtime monitoring provides an ongoing mechanism for detecting and responding to emerging threats. This case study underscores the importance of a defense-in-depth strategy, combining application-level security best practices with secure infrastructure management, especially for high-traffic enterprise applications handling sensitive data.