Skip to content

HSTS header never set in helmet middleware due to incorrect protocol check in gofiber/fiber

Moderate
ReneWerner87 published GHSA-gv83-gqw6-9j2c Jul 2, 2026

Package

gomod github.com/gofiber/fiber (Go)

Affected versions

<=v3.3.0

Patched versions

>=v3.4.0

Description

Summary

The helmet middleware in gofiber/fiber never sets the Strict-Transport-Security (HSTS) response header, even when HSTSMaxAge is explicitly configured, because the condition check at helmet.go:67 uses c.Protocol() — which returns the HTTP protocol version string (e.g., "HTTP/1.1", "HTTP/2.0") — instead of c.Scheme() — which returns the URL scheme ("http" or "https"). Since c.Protocol() never equals "https" in any real deployment, the HSTS header is permanently disabled, defeating the security protection.

Details

Root cause: middleware/helmet/helmet.go, line 67:

if c.Protocol() == "https" && cfg.HSTSMaxAge != 0 {

c.Protocol() (defined at req.go:865-867) delegates to fasthttp.Request.Header.Protocol(), which returns the HTTP protocol version:

  • "HTTP/1.1" for HTTP/1.1 connections
  • "HTTP/2.0" for HTTP/2 connections

The correct method is c.Scheme() (defined at req.go:844-862), which returns:

  • "http" for plain HTTP connections
  • "https" for TLS connections

Since "HTTP/1.1" != "https" always evaluates to true, the entire HSTS block (lines 67-76) is dead code.

Note on test coverage: The existing helmet test (helmet_test.go) passes because it uses ctx.Request.Header.SetProtocol("https") to artificially force Protocol() to return "https". However, fasthttp.Request.Header.SetProtocol() sets the HTTP version field, and real HTTP requests never have protocol "https" — they have "HTTP/1.1" or "HTTP/2.0". The test is validating the wrong thing.

PoC

Clean-checkout maintainer-runnable recipe:

  1. Save the following as middleware/helmet/poc_hsts_test.go:
package helmet

import (
    "crypto/tls"
    "net/http/httptest"
    "testing"

    "github.com/gofiber/fiber/v3"
)

func Test_PoC_HSTS_NeverSet(t *testing.T) {
    app := fiber.New()
    app.Use(New(Config{
        HSTSMaxAge: 31536000,
    }))
    app.Get("/", func(c fiber.Ctx) error {
        return c.SendString("ok")
    })

    // Simulate HTTPS connection
    req := httptest.NewRequest(fiber.MethodGet, "/", nil)
    req.TLS = &tls.ConnectionState{}

    resp, _ := app.Test(req)
    hsts := resp.Header.Get("Strict-Transport-Security")

    if hsts == "" {
        t.Log("BUG CONFIRMED: HSTS header not set. c.Protocol() returns 'HTTP/1.1', not 'https'")
        t.Log("Fix: change c.Protocol() == 'https' to c.Scheme() == 'https' on line 67")
    }
}
  1. Run: go test -run Test_PoC_HSTS_NeverSet -v ./middleware/helmet/

Expected vulnerable output:

=== RUN   Test_PoC_HSTS_NeverSet
    BUG CONFIRMED: HSTS header not set. c.Protocol() returns 'HTTP/1.1', not 'https'
    Fix: change c.Protocol() == 'https' to c.Scheme() == 'https' on line 67
--- PASS: Test_PoC_HSTS_NeverSet

Expected output after fix:

=== RUN   Test_PoC_HSTS_NeverSet
--- PASS: Test_PoC_HSTS_NeverSet
    (HSTS header is set: "max-age=31536000; includeSubDomains")

Observed output from this environment (commit ee98695f):

=== RUN   Test_PoC_HSTS_NeverSet
    poc_hsts_test.go:39: HSTS header value: ""
    poc_hsts_test.go:42: BUG CONFIRMED: HSTS header is NOT set even over TLS
    poc_hsts_test.go:43: Root cause: helmet.go:67 uses c.Protocol() which returns HTTP version
    poc_hsts_test.go:44: c.Protocol() returns 'HTTP/1.1' not 'https'
    poc_hsts_test.go:45: Fix: use c.Scheme() == 'https' instead of c.Protocol() == 'https'
--- PASS: Test_PoC_HSTS_NeverSet

Negative/control case: With HSTSMaxAge: 0 (default), HSTS is correctly not set (this is expected behavior, not a bug).

Cleanup: Remove poc_hsts_test.go after verification.

Impact

The HSTS header is never applied in production, leaving all users vulnerable to:

  • SSL stripping attacks: An active network attacker can downgrade HTTPS connections to HTTP, intercepting traffic between the client and server.
  • Protocol downgrade: Without HSTS, browsers will silently accept HTTP connections to the site, even if the site supports HTTPS.
  • Cookie theft over HTTP: Session cookies without the Secure flag will be sent over HTTP if the user is tricked into an HTTP connection.

This affects any application that:

  1. Uses the helmet middleware
  2. Configures HSTSMaxAge > 0 expecting HSTS protection
  3. Serves traffic over HTTPS

The vulnerability requires an active MITM attacker on the network path, which is realistic in public Wi-Fi, corporate networks, and ISP-level scenarios.

Suggested remediation

In middleware/helmet/helmet.go, line 67, replace c.Protocol() with c.Scheme():

// Before (broken):
if c.Protocol() == "https" && cfg.HSTSMaxAge != 0 {

// After (fixed):
if c.Scheme() == "https" && cfg.HSTSMaxAge != 0 {

Additionally, update the existing test to use a realistic TLS simulation instead of SetProtocol("https"):

// Before (artificial - sets HTTP version to "https" which never happens in practice):
ctx.Request.Header.SetProtocol("https")

// After (realistic - simulates TLS connection):
ctx.RequestCtx().Request.Header.SetProtocol("HTTP/1.1")
ctx.RequestCtx().TLS = &tls.ConnectionState{}

Regression test: Add a test case that verifies HSTS is set when req.TLS is non-nil and HSTSMaxAge > 0, without using SetProtocol.

Credits

  • Thai Son Dinh from VinSOC Labs (R&D)
  • Nguyen Huy Vu Dung from VinSOC Labs (AppSec)

Severity

Moderate

CVSS overall score

This score calculates overall vulnerability severity from 0 to 10 and is based on the Common Vulnerability Scoring System (CVSS).
/ 10

CVSS v3 base metrics

Attack vector
Network
Attack complexity
High
Privileges required
None
User interaction
None
Scope
Unchanged
Confidentiality
Low
Integrity
Low
Availability
None

CVSS v3 base metrics

Attack vector: More severe the more the remote (logically and physically) an attacker can be in order to exploit the vulnerability.
Attack complexity: More severe for the least complex attacks.
Privileges required: More severe if no privileges are required.
User interaction: More severe when no user interaction is required.
Scope: More severe when a scope change occurs, e.g. one vulnerable component impacts resources in components beyond its security scope.
Confidentiality: More severe when loss of data confidentiality is highest, measuring the level of data access available to an unauthorized user.
Integrity: More severe when loss of data integrity is the highest, measuring the consequence of data modification possible by an unauthorized user.
Availability: More severe when the loss of impacted component availability is highest.
CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:L/I:L/A:N

CVE ID

CVE-2026-53624

Weaknesses

Cleartext Transmission of Sensitive Information

The product transmits sensitive or security-critical data in cleartext in a communication channel that can be sniffed by unauthorized actors. Learn more on MITRE.

Credits