Web Security

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. 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. 2

    Call `URL::forceScheme('https')` in `AppServiceProvider::boot()` for production to ensure all generated URLs (pagination, redirects, API links) use HTTPS.

  3. 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. 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. 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

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