Fix HasSecureHeadersDefined returning false when stsSeconds is 0
Some checks failed
CodeQL / Analyze (push) Has been cancelled
Build and Publish Documentation / Doc Process (push) Has been cancelled
Build experimental image on branch / build-webui (push) Has been cancelled
Build experimental image on branch / Build experimental image on branch (push) Has been cancelled

This commit is contained in:
Varun Chawla 2026-03-03 07:12:05 -08:00 committed by GitHub
parent a71d4d640f
commit 7a95bac64f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 83 additions and 14 deletions

View file

@ -5,6 +5,7 @@ import (
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"k8s.io/utils/ptr"
)
func Test_GetStrategy_ipv6Subnet(t *testing.T) {
@ -19,16 +20,16 @@ func Test_GetStrategy_ipv6Subnet(t *testing.T) {
{
desc: "Zero subnet",
expectError: true,
ipv6Subnet: intPtr(0),
ipv6Subnet: ptr.To(0),
},
{
desc: "Subnet greater that 128",
expectError: true,
ipv6Subnet: intPtr(129),
ipv6Subnet: ptr.To(129),
},
{
desc: "Valid subnet",
ipv6Subnet: intPtr(128),
ipv6Subnet: ptr.To(128),
},
}
@ -52,6 +53,58 @@ func Test_GetStrategy_ipv6Subnet(t *testing.T) {
}
}
func intPtr(value int) *int {
return &value
func TestHasSecureHeadersDefined(t *testing.T) {
testCases := []struct {
desc string
headers *Headers
expected bool
}{
{
desc: "Nil headers",
headers: nil,
expected: false,
},
{
desc: "Empty headers",
headers: &Headers{},
expected: false,
},
{
desc: "STSSeconds set to non-zero",
headers: &Headers{
STSSeconds: ptr.To(int64(42)),
},
expected: true,
},
{
desc: "STSSeconds set to zero",
headers: &Headers{
STSSeconds: ptr.To(int64(0)),
},
expected: true,
},
{
desc: "STSSeconds nil (not set)",
headers: &Headers{
FrameDeny: true,
},
expected: true,
},
{
desc: "Only ForceSTSHeader",
headers: &Headers{
ForceSTSHeader: true,
},
expected: true,
},
}
for _, test := range testCases {
t.Run(test.desc, func(t *testing.T) {
t.Parallel()
result := test.headers.HasSecureHeadersDefined()
assert.Equal(t, test.expected, result)
})
}
}

View file

@ -331,7 +331,7 @@ type Headers struct {
// STSSeconds defines the max-age of the Strict-Transport-Security header.
// If set to 0, the header is not set.
// +kubebuilder:validation:Minimum=0
STSSeconds int64 `json:"stsSeconds,omitempty" toml:"stsSeconds,omitempty" yaml:"stsSeconds,omitempty" export:"true"`
STSSeconds *int64 `json:"stsSeconds,omitempty" toml:"stsSeconds,omitempty" yaml:"stsSeconds,omitempty" export:"true"`
// STSIncludeSubdomains defines whether the includeSubDomains directive is appended to the Strict-Transport-Security header.
STSIncludeSubdomains bool `json:"stsIncludeSubdomains,omitempty" toml:"stsIncludeSubdomains,omitempty" yaml:"stsIncludeSubdomains,omitempty" export:"true"`
// STSPreload defines whether the preload flag is appended to the Strict-Transport-Security header.
@ -407,7 +407,7 @@ func (h *Headers) HasSecureHeadersDefined() bool {
(h.SSLForceHost != nil && *h.SSLForceHost) ||
(h.SSLHost != nil && *h.SSLHost != "") ||
len(h.SSLProxyHeaders) != 0 ||
h.STSSeconds != 0 ||
h.STSSeconds != nil ||
h.STSIncludeSubdomains ||
h.STSPreload ||
h.ForceSTSHeader ||

View file

@ -660,6 +660,11 @@ func (in *Headers) DeepCopyInto(out *Headers) {
(*out)[key] = val
}
}
if in.STSSeconds != nil {
in, out := &in.STSSeconds, &out.STSSeconds
*out = new(int64)
**out = **in
}
if in.FeaturePolicy != nil {
in, out := &in.FeaturePolicy, &out.FeaturePolicy
*out = new(string)

View file

@ -640,7 +640,7 @@ func TestDecodeConfiguration(t *testing.T) {
"name1": "foobar",
},
SSLForceHost: pointer(true),
STSSeconds: 42,
STSSeconds: pointer(int64(42)),
STSIncludeSubdomains: true,
STSPreload: true,
ForceSTSHeader: true,
@ -1195,7 +1195,7 @@ func TestEncodeConfiguration(t *testing.T) {
"name1": "foobar",
},
SSLForceHost: pointer(true),
STSSeconds: 42,
STSSeconds: pointer(int64(42)),
STSIncludeSubdomains: true,
STSPreload: true,
ForceSTSHeader: true,

View file

@ -13,6 +13,7 @@ import (
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/traefik/traefik/v3/pkg/config/dynamic"
"k8s.io/utils/ptr"
)
func TestNew_withoutOptions(t *testing.T) {
@ -24,6 +25,14 @@ func TestNew_withoutOptions(t *testing.T) {
assert.Nil(t, mid)
}
func TestNew_withSTSSecondsZero(t *testing.T) {
next := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusOK) })
mid, err := New(t.Context(), next, dynamic.Headers{STSSeconds: ptr.To(int64(0))}, "testing")
require.NoError(t, err)
assert.NotNil(t, mid)
}
func TestNew_allowedHosts(t *testing.T) {
testCases := []struct {
desc string

View file

@ -6,6 +6,7 @@ import (
"github.com/traefik/traefik/v3/pkg/config/dynamic"
"github.com/traefik/traefik/v3/pkg/middlewares"
"github.com/unrolled/secure"
"k8s.io/utils/ptr"
)
type secureHeader struct {
@ -33,7 +34,7 @@ func newSecure(next http.Handler, cfg dynamic.Headers, contextKey string) *secur
AllowedHosts: cfg.AllowedHosts,
HostsProxyHeaders: cfg.HostsProxyHeaders,
SSLProxyHeaders: cfg.SSLProxyHeaders,
STSSeconds: cfg.STSSeconds,
STSSeconds: ptr.Deref(cfg.STSSeconds, 0),
PermissionsPolicy: cfg.PermissionsPolicy,
SecureContextKey: contextKey,
}

View file

@ -7,6 +7,7 @@ import (
"github.com/stretchr/testify/assert"
"github.com/traefik/traefik/v3/pkg/config/dynamic"
"k8s.io/utils/ptr"
)
// Middleware tests based on https://github.com/unrolled/secure
@ -27,7 +28,7 @@ func Test_newSecure_modifyResponse(t *testing.T) {
{
desc: "STSSeconds",
cfg: dynamic.Headers{
STSSeconds: 1,
STSSeconds: ptr.To(int64(1)),
ForceSTSHeader: true,
},
expected: http.Header{"Strict-Transport-Security": []string{"max-age=1"}},
@ -35,7 +36,7 @@ func Test_newSecure_modifyResponse(t *testing.T) {
{
desc: "STSSeconds and STSPreload",
cfg: dynamic.Headers{
STSSeconds: 1,
STSSeconds: ptr.To(int64(1)),
ForceSTSHeader: true,
STSPreload: true,
},

View file

@ -621,7 +621,7 @@ func Test_buildConfiguration(t *testing.T) {
"name0": "foobar",
},
SSLForceHost: pointer(true),
STSSeconds: 42,
STSSeconds: pointer(int64(42)),
STSIncludeSubdomains: true,
STSPreload: true,
ForceSTSHeader: true,

View file

@ -209,7 +209,7 @@ func init() {
AllowedHosts: []string{"foo"},
HostsProxyHeaders: []string{"foo"},
SSLProxyHeaders: map[string]string{"foo": "bar"},
STSSeconds: 42,
STSSeconds: pointer(int64(42)),
STSIncludeSubdomains: true,
STSPreload: true,
ForceSTSHeader: true,