Skip to content

Commit c48b851

Browse files
munishchouhanpditommasoclaude
authored
[COMP-1143]Missing HTTP Security Header (#955)
Signed-off-by: munishchouhan <hrma017@gmail.com> Co-authored-by: Paolo Di Tommaso <paolo.ditommaso@gmail.com> Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
1 parent b1446d4 commit c48b851

File tree

5 files changed

+429
-0
lines changed

5 files changed

+429
-0
lines changed
Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
/*
2+
* Wave, containers provisioning service
3+
* Copyright (c) 2023-2024, Seqera Labs
4+
*
5+
* This program is free software: you can redistribute it and/or modify
6+
* it under the terms of the GNU Affero General Public License as published by
7+
* the Free Software Foundation, either version 3 of the License, or
8+
* (at your option) any later version.
9+
*
10+
* This program is distributed in the hope that it will be useful,
11+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
12+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13+
* GNU Affero General Public License for more details.
14+
*
15+
* You should have received a copy of the GNU Affero General Public License
16+
* along with this program. If not, see <https://www.gnu.org/licenses/>.
17+
*/
18+
19+
package io.seqera.wave.configuration
20+
21+
import javax.annotation.Nullable
22+
import javax.annotation.PostConstruct
23+
24+
import groovy.transform.CompileStatic
25+
import groovy.transform.ToString
26+
import groovy.util.logging.Slf4j
27+
import io.micronaut.context.annotation.Requires
28+
import io.micronaut.context.annotation.Value
29+
import jakarta.inject.Singleton
30+
31+
/**
32+
* Configuration for HTTP security headers
33+
*
34+
* @author Munish Chouhan <munish.chouhan@seqera.io>
35+
*/
36+
@ToString(includeNames = true, includePackage = false)
37+
@CompileStatic
38+
@Slf4j
39+
@Singleton
40+
@Requires(property = 'wave.security.http-headers.enabled', value = 'true', defaultValue = 'true')
41+
class SecurityHeadersConfig {
42+
43+
/**
44+
* Enable or disable security headers globally
45+
*/
46+
@Value('${wave.security.http-headers.enabled:true}')
47+
Boolean enabled
48+
49+
/**
50+
* HSTS max-age in seconds
51+
* default to 1 year (31536000 seconds)
52+
* This tell browsers to only use HTTPS for one year (31,536,000 seconds) or custom value set by user,
53+
* preventing downgrade attacks and improving security by ensuring encrypted connections by default,
54+
* for more information check https://hstspreload.org/#submission-requirements
55+
*/
56+
@Value('${wave.security.http-headers.hsts.max-age:31536000}')
57+
Long hstsMaxAge
58+
59+
/**
60+
* Include subdomains in HSTS
61+
*/
62+
@Value('${wave.security.http-headers.hsts.include-sub-domains:true}')
63+
Boolean hstsIncludeSubDomains
64+
65+
/**
66+
* X-Frame-Options header value (default: `DENY`)
67+
*/
68+
@Nullable
69+
@Value('${wave.security.http-headers.frame-options}')
70+
String frameOptions
71+
72+
/**
73+
* X-Content-Type-Options header value
74+
*/
75+
@Nullable
76+
@Value('${wave.security.http-headers.content-type-options}')
77+
String contentTypeOptions
78+
79+
/**
80+
* Referrer-Policy header value
81+
*/
82+
@Nullable
83+
@Value('${wave.security.http-headers.referrer-policy}')
84+
String referrerPolicy
85+
86+
/**
87+
* Permissions-Policy header value
88+
*/
89+
@Nullable
90+
@Value('${wave.security.http-headers.permissions-policy}')
91+
String permissionsPolicy
92+
93+
/**
94+
* Content-Security-Policy header value
95+
*/
96+
@Nullable
97+
@Value('${wave.security.http-headers.content-security-policy}')
98+
String contentSecurityPolicy
99+
100+
@PostConstruct
101+
private void init() {
102+
log.info("Security headers config: enabled=${enabled}; hsts-max-age=${hstsMaxAge}; hsts-include-sub-domains=${hstsIncludeSubDomains}; " +
103+
"frame-options=${frameOptions}; content-type-options=${contentTypeOptions}; referrer-policy=${referrerPolicy}; " +
104+
"permissions-policy=${permissionsPolicy}; csp=${contentSecurityPolicy}")
105+
}
106+
107+
/**
108+
* Build the HSTS header value
109+
*/
110+
String getHstsValue() {
111+
if (hstsMaxAge == null) {
112+
return null
113+
}
114+
final result = new StringBuilder("max-age=${hstsMaxAge}")
115+
if (hstsIncludeSubDomains) {
116+
result.append("; includeSubDomains")
117+
}
118+
return result.toString()
119+
}
120+
}

src/main/groovy/io/seqera/wave/filter/FilterOrder.groovy

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ package io.seqera.wave.filter
2727
*/
2828
interface FilterOrder {
2929

30+
final int SECURITY_HEADERS = -120
3031
final int DENY_CRAWLER = -110
3132
final int DENY_PATHS = -100
3233
final int RATE_LIMITER = -50
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
/*
2+
* Wave, containers provisioning service
3+
* Copyright (c) 2023-2024, Seqera Labs
4+
*
5+
* This program is free software: you can redistribute it and/or modify
6+
* it under the terms of the GNU Affero General Public License as published by
7+
* the Free Software Foundation, either version 3 of the License, or
8+
* (at your option) any later version.
9+
*
10+
* This program is distributed in the hope that it will be useful,
11+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
12+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13+
* GNU Affero General Public License for more details.
14+
*
15+
* You should have received a copy of the GNU Affero General Public License
16+
* along with this program. If not, see <https://www.gnu.org/licenses/>.
17+
*/
18+
19+
package io.seqera.wave.filter
20+
21+
import groovy.transform.CompileStatic
22+
import groovy.util.logging.Slf4j
23+
import io.micronaut.context.annotation.Requires
24+
import io.micronaut.core.order.Ordered
25+
import io.micronaut.http.HttpResponse
26+
import io.micronaut.http.MutableHttpResponse
27+
import io.micronaut.http.annotation.ResponseFilter
28+
import io.micronaut.http.annotation.ServerFilter
29+
import io.seqera.wave.configuration.SecurityHeadersConfig
30+
import jakarta.inject.Inject
31+
import static io.micronaut.http.annotation.ServerFilter.MATCH_ALL_PATTERN
32+
33+
/**
34+
* HTTP filter to add security headers to all responses
35+
*
36+
* @author Munish Chouhan <munish.chouhan@seqera.io>
37+
*/
38+
@Slf4j
39+
@CompileStatic
40+
@ServerFilter(MATCH_ALL_PATTERN)
41+
@Requires(property = 'wave.security.http-headers.enabled', value = 'true', defaultValue = 'true')
42+
class SecurityHeadersFilter implements Ordered {
43+
44+
@Inject
45+
private SecurityHeadersConfig config
46+
47+
@Override
48+
int getOrder() {
49+
return FilterOrder.SECURITY_HEADERS
50+
}
51+
52+
@ResponseFilter
53+
void responseFilter(HttpResponse<?> response) {
54+
if (response instanceof MutableHttpResponse) {
55+
addSecurityHeaders((MutableHttpResponse<?>) response)
56+
}
57+
}
58+
59+
/**
60+
* Add security headers to the HTTP response
61+
*
62+
* @param response The mutable HTTP response to add headers to
63+
*/
64+
protected void addSecurityHeaders(MutableHttpResponse<?> response) {
65+
// Add HSTS header
66+
final hstsValue = config.getHstsValue()
67+
if (hstsValue) {
68+
response.header('Strict-Transport-Security', hstsValue)
69+
}
70+
71+
// Add X-Frame-Options
72+
if (config.frameOptions) {
73+
response.header('X-Frame-Options', config.frameOptions)
74+
}
75+
76+
// Add X-Content-Type-Options
77+
if (config.contentTypeOptions) {
78+
response.header('X-Content-Type-Options', config.contentTypeOptions)
79+
}
80+
81+
// Add Referrer-Policy
82+
if (config.referrerPolicy) {
83+
response.header('Referrer-Policy', config.referrerPolicy)
84+
}
85+
86+
// Add Permissions-Policy
87+
if (config.permissionsPolicy) {
88+
response.header('Permissions-Policy', config.permissionsPolicy)
89+
}
90+
91+
// Add Content-Security-Policy
92+
if (config.contentSecurityPolicy) {
93+
response.header('Content-Security-Policy', config.contentSecurityPolicy)
94+
}
95+
96+
log.trace "Added security headers to response"
97+
}
98+
}

src/main/resources/application.yml

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,17 @@ wave:
5050
allowAnonymous: true
5151
server:
5252
url: "${WAVE_SERVER_URL:`http://localhost:9090`}"
53+
security:
54+
http-headers:
55+
enabled: true
56+
hsts:
57+
max-age: 31536000
58+
include-sub-domains: true
59+
frame-options: "DENY"
60+
content-type-options: "nosniff"
61+
referrer-policy: "strict-origin-when-cross-origin"
62+
permissions-policy: "camera=(), microphone=(), geolocation=()"
63+
content-security-policy: "default-src 'self'; frame-ancestors 'none'"
5364
build:
5465
buildkit-image: "public.cr.seqera.io/wave/buildkit:v0.25.2-rootless"
5566
singularity-image: "public.cr.seqera.io/wave/singularity:v4.2.1-r4"

0 commit comments

Comments
 (0)