What Is HTTP Strict Transport Security (HSTS)?
A security header that tells browsers to only connect to your website over HTTPS. Once a browser receives the HSTS header, it will automatically convert all future HTTP requests to HTTPS for the specified duration, preventing protocol downgrade attacks and cookie hijacking.
How It Works
HSTS works by shifting responsibility for HTTPS enforcement from the server to the browser, eliminating the window of vulnerability in the initial HTTP request.
Step 1: First HTTPS connection — A user visits https://yourapp.com for the first time. The server responds with the page content plus the header: Strict-Transport-Security: max-age=31536000; includeSubDomains.
Step 2: Browser stores the policy — The browser records that yourapp.com and all its subdomains require HTTPS, and this policy is valid for 31,536,000 seconds (one year) from the current time.
Step 3: Subsequent requests — The user types yourapp.com in their browser address bar (no https:// prefix) or clicks a http:// link. Before any network packet is sent, the browser checks its HSTS store, finds the policy, and internally rewrites the request to https://yourapp.com. The HTTP request never leaves the device.
Step 4: SSL stripping is blocked — Without HSTS, an attacker on the network path can intercept the initial http://yourapp.com request before HTTPS negotiation begins and present a plain HTTP connection to the victim. With HSTS, there is no initial HTTP request to intercept — the browser sends HTTPS directly.
Step 5: Policy expiry and renewal — Each successful HTTPS response with the HSTS header resets the max-age timer. As long as users visit your site at least once a year, the policy remains continuously enforced. If you need to disable HSTS (for maintenance or subdomain changes), you must set max-age=0 in the header and wait for all browsers' cached policies to expire.
Preloading — The preload directive in the HSTS header, combined with submitting your domain to hstspreload.org, causes browsers to include your domain in their hardcoded HSTS list. This means the policy is enforced even on the very first visit — browsers know to use HTTPS before they have ever connected to your site.
Types of HTTP Strict Transport Security (HSTS)
Missing HSTS Header
Application serves HTTPS but does not send the HSTS header — initial HTTP requests are vulnerable to SSL stripping by network attackers.
# Check if HSTS is missing
curl -sI https://yourapp.com | grep -i strict
# Empty output = no HSTS = vulnerable to first-request interception
Insufficient max-age
HSTS is set with a very short max-age (e.g., 300 seconds) — the policy expires quickly, leaving gaps where the application is vulnerable between visits.
# Weak: policy expires in 5 minutes, gaps between visits are unprotected
Strict-Transport-Security: max-age=300
# Strong: policy valid for one year, continuously renewed
Strict-Transport-Security: max-age=31536000; includeSubDomains
Missing includeSubDomains
HSTS policy covers only the apex domain but not subdomains — API subdomain (`api.yourapp.com`) or staging subdomain remain vulnerable to SSL stripping.
# Missing includeSubDomains leaves subdomains vulnerable
Strict-Transport-Security: max-age=31536000
# Subdomains not covered: api.yourapp.com, staging.yourapp.com unprotected
In Laravel Applications
Set the HSTS header in Laravel middleware: $response->headers->set("Strict-Transport-Security", "max-age=31536000; includeSubDomains"). Also force HTTPS in production with URL::forceScheme("https") in your AppServiceProvider.
Code Examples
HSTS Header in Laravel Middleware
Vulnerable
// No HSTS header — application is vulnerable to SSL stripping
// AppServiceProvider::boot() — only basic HTTPS redirect, no HSTS
public function boot(): void
{
if ($this->app->environment('production')) {
\Illuminate\Support\Facades\URL::forceScheme('https');
}
// Missing: HSTS header in HTTP responses
}
Secure
// app/Http/Middleware/SecurityHeaders.php — add HSTS header to all responses
public function handle(Request $request, Closure $next): Response
{
$response = $next($request);
if (app()->environment('production')) {
$response->headers->set(
'Strict-Transport-Security',
'max-age=31536000; includeSubDomains; preload'
);
}
return $response;
}
// AppServiceProvider::boot() — force HTTPS for URL generation
public function boot(): void
{
if ($this->app->environment('production')) {
URL::forceScheme('https');
}
}
Nginx HSTS Configuration
Vulnerable
# Nginx without HSTS — redirect to HTTPS but no browser-level enforcement
server {
listen 80;
server_name yourapp.com;
return 301 https://$host$request_uri;
}
server {
listen 443 ssl;
# No add_header Strict-Transport-Security — vulnerable to SSL strip
Secure
# Nginx with HSTS — browser never makes plain HTTP request after first visit
server {
listen 80;
server_name yourapp.com www.yourapp.com;
return 301 https://yourapp.com$request_uri;
}
server {
listen 443 ssl http2;
server_name yourapp.com;
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always;
# "always" ensures header is sent even on error responses
}
Real-World Example
Without HSTS, an attacker on a public WiFi network could intercept the initial HTTP request to your site and redirect the user to a fake version. With HSTS, the browser refuses to connect over HTTP.
Why It Matters
HSTS solves a specific and dangerous problem: the initial HTTP request. Even if your application serves HTTPS perfectly, a user who types yourapp.com in their browser makes an initial HTTP request before being redirected to HTTPS. This initial HTTP request is vulnerable to interception by an attacker who can downgrade the connection or redirect the user to a fake site.
With HSTS, browsers remember that your site requires HTTPS. After the first successful HTTPS connection where the HSTS header is received, the browser converts all future requests to HTTPS before they even leave the user's device. This eliminates the vulnerability window of the initial request.
In Laravel, HSTS is set via middleware. The header should include a long max-age (31536000 seconds, or one year, is standard), the includeSubDomains directive (so subdomains are also protected), and ideally preload (which submits your domain to browser preload lists so the protection applies even on the first ever visit).
A common mistake in Laravel is setting HSTS correctly in the middleware but failing to force HTTPS in the application itself. URL::forceScheme('https') in AppServiceProvider::boot() ensures that all generated URLs use HTTPS in production, which prevents mixed content issues and internal redirects back to HTTP.
Common Misconceptions
Myth: If our server redirects HTTP to HTTPS, HSTS is redundant.
Reality: The HTTP-to-HTTPS redirect is itself the vulnerable step — it occurs over an unencrypted connection that an attacker can intercept. HSTS prevents the browser from ever making that initial HTTP request.
Myth: HSTS only matters for login pages.
Reality: HSTS applies to the entire domain. Any HTTP request to any page on your domain before HSTS is established is vulnerable. Session cookies, authentication tokens, and sensitive query parameters can all be stolen over HTTP.
Myth: Setting `includeSubDomains` in HSTS is risky.
Reality: `includeSubDomains` should be included when all your subdomains serve HTTPS. Excluding it leaves subdomain traffic vulnerable. Only omit it if a specific subdomain requires HTTP for a legitimate reason.
How to Detect This
Run `curl -I http://yourdomain.com` (note: HTTP, not HTTPS) to verify you get a redirect to HTTPS, then run `curl -I https://yourdomain.com` to check that the `Strict-Transport-Security` header is present in the HTTPS response. The `max-age` should be at least 31536000. Check that `includeSubDomains` is present. In Laravel, verify that `URL::forceScheme('https')` is called in `AppServiceProvider::boot()` for production and that your session config has `'secure' => true` in `config/session.php`. StackShield automatically checks HSTS presence, correctness, and `max-age` value on every scan and alerts if the header is missing or weakened after a deployment.
How to Prevent This in Laravel
-
1
Add `Strict-Transport-Security: max-age=31536000; includeSubDomains` in a security headers middleware registered globally in `app/Http/Kernel.php` — gate the header on `app()->environment('production')` to avoid locking developers out during local HTTP development.
-
2
Call `URL::forceScheme('https')` in `AppServiceProvider::boot()` for production to ensure all generated URLs (pagination, redirects, API links) use HTTPS.
-
3
Set `SESSION_SECURE_COOKIE=true` in your production `.env` and verify `config/session.php` has `'secure' => env('SESSION_SECURE_COOKIE', false)` to prevent session cookies from traveling over HTTP.
-
4
Verify HSTS is working with `curl -sI https://yourapp.com | grep -i strict` — the response must include `Strict-Transport-Security` with at least `max-age=31536000`.
-
5
Submit your domain to hstspreload.org after setting `preload` in the header — this adds your domain to browser HSTS preload lists for protection on first-ever visits before any HSTS header can be received.
Frequently Asked Questions
How do I add HSTS in Laravel?
Add `Strict-Transport-Security` to a security headers middleware class that calls `$response->headers->set('Strict-Transport-Security', 'max-age=31536000; includeSubDomains')`, register this middleware in the global `$middleware` array in `app/Http/Kernel.php`, and gate it on `app()->environment('production')`. Additionally, call `URL::forceScheme('https')` in `AppServiceProvider::boot()` to ensure internally generated URLs use HTTPS.
What max-age value should I use for HSTS?
31536000 seconds (one year) is the standard for production applications and the minimum required to submit to the HSTS preload list. Start with a shorter value (e.g., 86400 for one day) during initial testing to verify no legitimate subdomains need HTTP access, then increase to 31536000 once confirmed. Using `max-age=0` revokes the policy, but browsers must make at least one successful connection to receive the revocation.
What does `includeSubDomains` do, and should I always include it?
`includeSubDomains` extends the HSTS policy to every subdomain of your root domain — `api.yourapp.com`, `staging.yourapp.com`, `mail.yourapp.com`. You should include it whenever all your subdomains serve HTTPS, which is best practice. Only omit it if a specific subdomain intentionally serves content over HTTP, which is rare and generally inadvisable in production.
How does HSTS preloading work?
HSTS preloading embeds your domain in a list that browsers ship with their source code — Chrome, Firefox, Safari, and Edge all maintain this list. Once on the preload list, the browser enforces HTTPS for your domain even on the very first visit, before receiving any HSTS header. Requirements for preloading: serve a valid HTTPS certificate, redirect HTTP to HTTPS, set `Strict-Transport-Security: max-age=31536000; includeSubDomains; preload`, then submit at hstspreload.org. Be aware that preloading is difficult to reverse.
Related Terms
Security Headers
HTTP response headers that instruct browsers how to handle your website's content securely. They protect against common attacks like cross-site scripting (XSS), clickjacking, and protocol downgrade attacks by telling the browser what actions are allowed.
SSL/TLS
Cryptographic protocols that provide encrypted communication between a client (browser) and server. SSL (Secure Sockets Layer) is the predecessor to TLS (Transport Layer Security). TLS 1.2 and 1.3 are the current standards. These protocols ensure data transmitted between users and your application cannot be intercepted or modified.
Man-in-the-Middle Attack (MITM)
An attack where an adversary secretly intercepts and potentially modifies communication between two parties who believe they are communicating directly with each other. The attacker can read, inject, or alter data in transit.
Monitor Your Laravel Application's Security
StackShield continuously checks your Laravel application from the outside, catching security issues before attackers find them.
Start Free Trial