KEYCLOAK-14232 Add Referrer-Policy: no-referrer to each response from Keycloak

(cherry picked from commit 0b49640231abc6e465542bd2608e1c908c079ced)
This commit is contained in:
mhajas 2020-06-17 14:31:08 +02:00 committed by Stian Thorgersen
parent f037dabdc1
commit f7e0af438d
11 changed files with 140 additions and 159 deletions

View file

@ -21,125 +21,54 @@ import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
/**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $
*/
public class BrowserSecurityHeaders {
public enum BrowserSecurityHeaders {
public static final String X_FRAME_OPTIONS = "X-Frame-Options";
X_FRAME_OPTIONS("xFrameOptions", "X-Frame-Options", "SAMEORIGIN"),
CONTENT_SECURITY_POLICY("contentSecurityPolicy", "Content-Security-Policy", ContentSecurityPolicyBuilder.create().build()),
CONTENT_SECURITY_POLICY_REPORT_ONLY("contentSecurityPolicyReportOnly", "Content-Security-Policy-Report-Only", ""),
X_CONTENT_TYPE_OPTIONS("xContentTypeOptions", "X-Content-Type-Options", "nosniff"),
X_ROBOTS_TAG("xRobotsTag", "X-Robots-Tag", "none"),
X_XSS_PROTECTION("xXSSProtection", "X-XSS-Protection", "1; mode=block"),
STRICT_TRANSPORT_SECURITY("strictTransportSecurity", "Strict-Transport-Security", "max-age=31536000; includeSubDomains"),
REFERRER_POLICY("referrerPolicy", "Referrer-Policy", "no-referrer"),
;
public static final String X_FRAME_OPTIONS_DEFAULT = "SAMEORIGIN";
private final String key;
private final String headerName;
private final String defaultValue;
public static final String X_FRAME_OPTIONS_KEY = "xFrameOptions";
BrowserSecurityHeaders(String key, String headerName, String defaultValue) {
this.key = key;
this.headerName = headerName;
this.defaultValue = defaultValue;
}
public static final String CONTENT_SECURITY_POLICY = "Content-Security-Policy";
public String getKey() {
return key;
}
public static final String CONTENT_SECURITY_POLICY_DEFAULT = ContentSecurityPolicyBuilder.create().build();
public String getHeaderName() {
return headerName;
}
public static final String CONTENT_SECURITY_POLICY_KEY = "contentSecurityPolicy";
public String getDefaultValue() {
return defaultValue;
}
public static final String CONTENT_SECURITY_POLICY_REPORT_ONLY = "Content-Security-Policy-Report-Only";
public static final String CONTENT_SECURITY_POLICY_REPORT_ONLY_DEFAULT = "";
public static final String CONTENT_SECURITY_POLICY_REPORT_ONLY_KEY = "contentSecurityPolicyReportOnly";
public static final String X_CONTENT_TYPE_OPTIONS = "X-Content-Type-Options";
public static final String X_CONTENT_TYPE_OPTIONS_DEFAULT = "nosniff";
public static final String X_CONTENT_TYPE_OPTIONS_KEY = "xContentTypeOptions";
public static final String X_ROBOTS_TAG = "X-Robots-Tag";
public static final String X_ROBOTS_TAG_KEY = "xRobotsTag";
public static final String X_ROBOTS_TAG_DEFAULT = "none";
public static final String X_XSS_PROTECTION = "X-XSS-Protection";
public static final String X_XSS_PROTECTION_DEFAULT = "1; mode=block";
public static final String X_XSS_PROTECTION_KEY = "xXSSProtection";
public static final String STRICT_TRANSPORT_SECURITY = "Strict-Transport-Security";
public static final String STRICT_TRANSPORT_SECURITY_DEFAULT = "max-age=31536000; includeSubDomains";
public static final String STRICT_TRANSPORT_SECURITY_KEY = "strictTransportSecurity";
public static final Map<String, String> headerAttributeMap;
public static final Map<String, String> defaultHeaders;
@Deprecated // should be removed eventually
public static final Map<String, String> realmDefaultHeaders;
static {
Map<String, String> headerMap = new HashMap<>();
headerMap.put(X_FRAME_OPTIONS_KEY, X_FRAME_OPTIONS);
headerMap.put(CONTENT_SECURITY_POLICY_KEY, CONTENT_SECURITY_POLICY);
headerMap.put(CONTENT_SECURITY_POLICY_REPORT_ONLY_KEY, CONTENT_SECURITY_POLICY_REPORT_ONLY);
headerMap.put(X_CONTENT_TYPE_OPTIONS_KEY, X_CONTENT_TYPE_OPTIONS);
headerMap.put(X_ROBOTS_TAG_KEY, X_ROBOTS_TAG);
headerMap.put(X_XSS_PROTECTION_KEY, X_XSS_PROTECTION);
headerMap.put(STRICT_TRANSPORT_SECURITY_KEY, STRICT_TRANSPORT_SECURITY);
Map<String, String> dh = new HashMap<>();
dh.put(X_FRAME_OPTIONS_KEY, X_FRAME_OPTIONS_DEFAULT);
dh.put(CONTENT_SECURITY_POLICY_KEY, CONTENT_SECURITY_POLICY_DEFAULT);
dh.put(CONTENT_SECURITY_POLICY_REPORT_ONLY_KEY, CONTENT_SECURITY_POLICY_REPORT_ONLY_DEFAULT);
dh.put(X_CONTENT_TYPE_OPTIONS_KEY, X_CONTENT_TYPE_OPTIONS_DEFAULT);
dh.put(X_ROBOTS_TAG_KEY, X_ROBOTS_TAG_DEFAULT);
dh.put(X_XSS_PROTECTION_KEY, X_XSS_PROTECTION_DEFAULT);
dh.put(STRICT_TRANSPORT_SECURITY_KEY, STRICT_TRANSPORT_SECURITY_DEFAULT);
dh.put(X_FRAME_OPTIONS.getKey(), X_FRAME_OPTIONS.getDefaultValue());
dh.put(CONTENT_SECURITY_POLICY.getKey(), CONTENT_SECURITY_POLICY.getDefaultValue());
dh.put(CONTENT_SECURITY_POLICY_REPORT_ONLY.getKey(), CONTENT_SECURITY_POLICY_REPORT_ONLY.getDefaultValue());
dh.put(X_CONTENT_TYPE_OPTIONS.getKey(), X_CONTENT_TYPE_OPTIONS.getDefaultValue());
dh.put(X_ROBOTS_TAG.getKey(), X_ROBOTS_TAG.getDefaultValue());
dh.put(X_XSS_PROTECTION.getKey(), X_XSS_PROTECTION.getDefaultValue());
dh.put(STRICT_TRANSPORT_SECURITY.getKey(), STRICT_TRANSPORT_SECURITY.getDefaultValue());
defaultHeaders = Collections.unmodifiableMap(dh);
headerAttributeMap = Collections.unmodifiableMap(headerMap);
}
public static class ContentSecurityPolicyBuilder {
private String frameSrc = "'self'";
private String frameAncestors = "'self'";
private String objectSrc = "'none'";
private boolean first;
private StringBuilder sb;
public static ContentSecurityPolicyBuilder create() {
return new ContentSecurityPolicyBuilder();
}
public ContentSecurityPolicyBuilder frameSrc(String frameSrc) {
this.frameSrc = frameSrc;
return this;
}
public ContentSecurityPolicyBuilder frameAncestors(String frameancestors) {
this.frameAncestors = frameancestors;
return this;
}
public String build() {
sb = new StringBuilder();
first = true;
build("frame-src", frameSrc);
build("frame-ancestors", frameAncestors);
build("object-src", objectSrc);
return sb.toString();
}
private void build(String k, String v) {
if (v != null) {
if (!first) {
sb.append(" ");
}
first = false;
sb.append(k).append(" ").append(v).append(";");
realmDefaultHeaders = Collections.unmodifiableMap(dh);
}
}
}
}

View file

@ -0,0 +1,48 @@
package org.keycloak.models;
public class ContentSecurityPolicyBuilder {
private String frameSrc = "'self'";
private String frameAncestors = "'self'";
private String objectSrc = "'none'";
private boolean first;
private StringBuilder sb;
public static ContentSecurityPolicyBuilder create() {
return new ContentSecurityPolicyBuilder();
}
public ContentSecurityPolicyBuilder frameSrc(String frameSrc) {
this.frameSrc = frameSrc;
return this;
}
public ContentSecurityPolicyBuilder frameAncestors(String frameancestors) {
this.frameAncestors = frameancestors;
return this;
}
public String build() {
sb = new StringBuilder();
first = true;
build("frame-src", frameSrc);
build("frame-ancestors", frameAncestors);
build("object-src", objectSrc);
return sb.toString();
}
private void build(String k, String v) {
if (v != null) {
if (!first) {
sb.append(" ");
}
first = false;
sb.append(k).append(" ").append(v).append(";");
}
}
}

View file

@ -387,7 +387,7 @@ public class RepresentationToModel {
if (rep.getBrowserSecurityHeaders() != null) {
newRealm.setBrowserSecurityHeaders(rep.getBrowserSecurityHeaders());
} else {
newRealm.setBrowserSecurityHeaders(BrowserSecurityHeaders.defaultHeaders);
newRealm.setBrowserSecurityHeaders(BrowserSecurityHeaders.realmDefaultHeaders);
}
if (rep.getComponents() != null) {

View file

@ -8,10 +8,10 @@ public class BrowserSecurityHeadersTest {
@Test
public void contentSecurityPolicyBuilderTest() {
assertEquals("frame-src 'self'; frame-ancestors 'self'; object-src 'none';", BrowserSecurityHeaders.ContentSecurityPolicyBuilder.create().build());
assertEquals("frame-ancestors 'self'; object-src 'none';", BrowserSecurityHeaders.ContentSecurityPolicyBuilder.create().frameSrc(null).build());
assertEquals("frame-src 'self'; object-src 'none';", BrowserSecurityHeaders.ContentSecurityPolicyBuilder.create().frameAncestors(null).build());
assertEquals("frame-src 'custom-frame-src'; frame-ancestors 'custom-frame-ancestors'; object-src 'none';", BrowserSecurityHeaders.ContentSecurityPolicyBuilder.create().frameSrc("'custom-frame-src'").frameAncestors("'custom-frame-ancestors'").build());
assertEquals("frame-src 'self'; frame-ancestors 'self'; object-src 'none';", ContentSecurityPolicyBuilder.create().build());
assertEquals("frame-ancestors 'self'; object-src 'none';", ContentSecurityPolicyBuilder.create().frameSrc(null).build());
assertEquals("frame-src 'self'; object-src 'none';", ContentSecurityPolicyBuilder.create().frameAncestors(null).build());
assertEquals("frame-src 'custom-frame-src'; frame-ancestors 'custom-frame-ancestors'; object-src 'none';", ContentSecurityPolicyBuilder.create().frameSrc("'custom-frame-src'").frameAncestors("'custom-frame-ancestors'").build());
}
}

View file

@ -18,6 +18,7 @@ package org.keycloak.headers;
import org.jboss.logging.Logger;
import org.keycloak.models.BrowserSecurityHeaders;
import org.keycloak.models.ContentSecurityPolicyBuilder;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
@ -26,8 +27,11 @@ import javax.ws.rs.container.ContainerRequestContext;
import javax.ws.rs.container.ContainerResponseContext;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.MultivaluedMap;
import java.util.Collections;
import java.util.Map;
import static org.keycloak.models.BrowserSecurityHeaders.CONTENT_SECURITY_POLICY;
public class DefaultSecurityHeadersProvider implements SecurityHeadersProvider {
private static final Logger LOGGER = Logger.getLogger(DefaultSecurityHeadersProvider.class);
@ -44,7 +48,7 @@ public class DefaultSecurityHeadersProvider implements SecurityHeadersProvider {
if (realm != null) {
headerValues = realm.getBrowserSecurityHeaders();
} else {
headerValues = BrowserSecurityHeaders.defaultHeaders;
headerValues = Collections.emptyMap();
}
}
@ -81,27 +85,31 @@ public class DefaultSecurityHeadersProvider implements SecurityHeadersProvider {
}
private void addGenericHeaders(MultivaluedMap<String, Object> headers) {
addHeader(BrowserSecurityHeaders.STRICT_TRANSPORT_SECURITY_KEY, headers);
addHeader(BrowserSecurityHeaders.X_CONTENT_TYPE_OPTIONS_KEY, headers);
addHeader(BrowserSecurityHeaders.X_XSS_PROTECTION_KEY, headers);
addHeader(BrowserSecurityHeaders.STRICT_TRANSPORT_SECURITY, headers);
addHeader(BrowserSecurityHeaders.X_CONTENT_TYPE_OPTIONS, headers);
addHeader(BrowserSecurityHeaders.X_XSS_PROTECTION, headers);
addHeader(BrowserSecurityHeaders.REFERRER_POLICY, headers);
}
private void addRestHeaders(MultivaluedMap<String, Object> headers) {
addHeader(BrowserSecurityHeaders.STRICT_TRANSPORT_SECURITY_KEY, headers);
addHeader(BrowserSecurityHeaders.X_FRAME_OPTIONS_KEY, headers);
addHeader(BrowserSecurityHeaders.X_CONTENT_TYPE_OPTIONS_KEY, headers);
addHeader(BrowserSecurityHeaders.X_XSS_PROTECTION_KEY, headers);
addHeader(BrowserSecurityHeaders.STRICT_TRANSPORT_SECURITY, headers);
addHeader(BrowserSecurityHeaders.X_FRAME_OPTIONS, headers);
addHeader(BrowserSecurityHeaders.X_CONTENT_TYPE_OPTIONS, headers);
addHeader(BrowserSecurityHeaders.X_XSS_PROTECTION, headers);
addHeader(BrowserSecurityHeaders.REFERRER_POLICY, headers);
}
private void addHtmlHeaders(MultivaluedMap<String, Object> headers) {
BrowserSecurityHeaders.headerAttributeMap.keySet().forEach(k -> addHeader(k, headers));
for (BrowserSecurityHeaders header : BrowserSecurityHeaders.values()) {
addHeader(header, headers);
}
// TODO This will be refactored as part of introducing a more strict CSP header
if (options != null) {
BrowserSecurityHeaders.ContentSecurityPolicyBuilder csp = BrowserSecurityHeaders.ContentSecurityPolicyBuilder.create();
ContentSecurityPolicyBuilder csp = ContentSecurityPolicyBuilder.create();
if (options.isAllowAnyFrameAncestor()) {
headers.remove(BrowserSecurityHeaders.X_FRAME_OPTIONS);
headers.remove(BrowserSecurityHeaders.X_FRAME_OPTIONS.getHeaderName());
csp.frameAncestors(null);
}
@ -111,17 +119,16 @@ public class DefaultSecurityHeadersProvider implements SecurityHeadersProvider {
csp.frameSrc(allowedFrameSrc);
}
if (BrowserSecurityHeaders.CONTENT_SECURITY_POLICY_DEFAULT.equals(headers.getFirst(BrowserSecurityHeaders.CONTENT_SECURITY_POLICY))) {
headers.putSingle(BrowserSecurityHeaders.CONTENT_SECURITY_POLICY, csp.build());
if (CONTENT_SECURITY_POLICY.getDefaultValue().equals(headers.getFirst(CONTENT_SECURITY_POLICY.getHeaderName()))) {
headers.putSingle(CONTENT_SECURITY_POLICY.getHeaderName(), csp.build());
}
}
}
private void addHeader(String key, MultivaluedMap<String, Object> headers) {
String header = BrowserSecurityHeaders.headerAttributeMap.get(key);
String value = headerValues.get(key);
private void addHeader(BrowserSecurityHeaders header, MultivaluedMap<String, Object> headers) {
String value = headerValues.getOrDefault(header.getKey(), header.getDefaultValue());
if (value != null && !value.isEmpty()) {
headers.putSingle(header, value);
headers.putSingle(header.getHeaderName(), value);
}
}

View file

@ -227,7 +227,7 @@ public class RealmManager {
protected void setupRealmDefaults(RealmModel realm) {
realm.setBrowserSecurityHeaders(BrowserSecurityHeaders.defaultHeaders);
realm.setBrowserSecurityHeaders(BrowserSecurityHeaders.realmDefaultHeaders);
// brute force
realm.setBruteForceProtected(false); // default settings off for now todo set it on

View file

@ -1,22 +1,20 @@
package org.keycloak.testsuite.admin;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpOptions;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClientBuilder;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.keycloak.models.BrowserSecurityHeaders;
import org.keycloak.services.resources.Cors;
import org.keycloak.testsuite.util.UserBuilder;
import javax.ws.rs.core.MultivaluedMap;
import javax.ws.rs.core.Response;
import java.io.IOException;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.is;
import static org.junit.Assert.assertThat;
public class AdminHeadersTest extends AbstractAdminTest {
@ -42,14 +40,19 @@ public class AdminHeadersTest extends AbstractAdminTest {
Response response = realm.users().create(UserBuilder.create().username("headers-user").build());
MultivaluedMap<String, Object> h = response.getHeaders();
assertEquals(BrowserSecurityHeaders.STRICT_TRANSPORT_SECURITY_DEFAULT, h.getFirst(BrowserSecurityHeaders.STRICT_TRANSPORT_SECURITY));
assertEquals(BrowserSecurityHeaders.X_FRAME_OPTIONS_DEFAULT, h.getFirst(BrowserSecurityHeaders.X_FRAME_OPTIONS));
assertEquals(BrowserSecurityHeaders.X_CONTENT_TYPE_OPTIONS_DEFAULT, h.getFirst(BrowserSecurityHeaders.X_CONTENT_TYPE_OPTIONS));
assertEquals(BrowserSecurityHeaders.X_XSS_PROTECTION_DEFAULT, h.getFirst(BrowserSecurityHeaders.X_XSS_PROTECTION));
assertDefaultValue(BrowserSecurityHeaders.STRICT_TRANSPORT_SECURITY, h);
assertDefaultValue(BrowserSecurityHeaders.X_FRAME_OPTIONS, h);
assertDefaultValue(BrowserSecurityHeaders.X_CONTENT_TYPE_OPTIONS, h);
assertDefaultValue(BrowserSecurityHeaders.X_XSS_PROTECTION, h);
assertDefaultValue(BrowserSecurityHeaders.REFERRER_POLICY, h);
response.close();
}
private void assertDefaultValue(BrowserSecurityHeaders header, MultivaluedMap<String, Object> h) {
assertThat(h.getFirst(header.getHeaderName()), is(equalTo(header.getDefaultValue())));
}
private String getAdminUrl(String resource) {
return suiteContext.getAuthServerInfo().getContextRoot().toString() + "/auth/admin/" + resource;
}

View file

@ -1,9 +1,7 @@
package org.keycloak.testsuite.error;
import org.apache.commons.io.IOUtils;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.client.methods.HttpRequestBase;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClientBuilder;
@ -14,7 +12,6 @@ import org.keycloak.admin.client.resource.RealmResource;
import org.keycloak.broker.provider.util.SimpleHttp;
import org.keycloak.common.util.StreamUtil;
import org.keycloak.models.BrowserSecurityHeaders;
import org.keycloak.representations.idm.ErrorRepresentation;
import org.keycloak.representations.idm.OAuth2ErrorRepresentation;
import org.keycloak.representations.idm.RealmRepresentation;
import org.keycloak.testsuite.AbstractKeycloakTest;
@ -31,7 +28,6 @@ import java.net.URI;
import java.nio.charset.Charset;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import static org.junit.Assert.*;
@ -126,14 +122,13 @@ public class UncaughtErrorPageTest extends AbstractKeycloakTest {
try (CloseableHttpClient client = HttpClientBuilder.create().build()) {
SimpleHttp.Response response = SimpleHttp.doGet(uri.toString(), client).header("Accept", MediaType.TEXT_HTML_UTF_8).asResponse();
for (Map.Entry<String, String> e : BrowserSecurityHeaders.headerAttributeMap.entrySet()) {
String header = e.getValue();
String expectedValue = BrowserSecurityHeaders.defaultHeaders.get(e.getKey());
for (BrowserSecurityHeaders header : BrowserSecurityHeaders.values()) {
String expectedValue = header.getDefaultValue();
if (expectedValue == null || expectedValue.isEmpty()) {
assertNull(response.getFirstHeader(header));
assertNull(response.getFirstHeader(header.getHeaderName()));
} else {
assertEquals(expectedValue, response.getFirstHeader(header));
assertEquals(expectedValue, response.getFirstHeader(header.getHeaderName()));
}
}
}

View file

@ -68,7 +68,6 @@ import javax.ws.rs.core.UriBuilder;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import org.apache.commons.lang3.RandomStringUtils;
@ -164,14 +163,14 @@ public class LoginTest extends AbstractTestRealmKeycloakTest {
Client client = ClientBuilder.newClient();
Response response = client.target(oauth.getLoginFormUrl()).request().get();
Assert.assertThat(response.getStatus(), is(equalTo(200)));
for (Map.Entry<String, String> entry : BrowserSecurityHeaders.defaultHeaders.entrySet()) {
String headerName = BrowserSecurityHeaders.headerAttributeMap.get(entry.getKey());
String headerValue = response.getHeaderString(headerName);
if (entry.getValue().isEmpty()) {
for (BrowserSecurityHeaders header : BrowserSecurityHeaders.values()) {
String headerValue = response.getHeaderString(header.getHeaderName());
String expectedValue = header.getDefaultValue();
if (expectedValue.isEmpty()) {
Assert.assertNull(headerValue);
} else {
Assert.assertNotNull(headerValue);
Assert.assertThat(headerValue, is(equalTo(entry.getValue())));
Assert.assertThat(headerValue, is(equalTo(expectedValue)));
}
}
response.close();

View file

@ -130,9 +130,9 @@ public class LoginStatusIframeEndpointTest extends AbstractKeycloakTest {
assertTrue(s.contains("function getCookie()"));
assertEquals("CP=\"This is not a P3P policy!\"", response.getFirstHeader("P3P").getValue());
assertNull(response.getFirstHeader(BrowserSecurityHeaders.X_FRAME_OPTIONS));
assertEquals("frame-src 'self'; object-src 'none';", response.getFirstHeader(BrowserSecurityHeaders.CONTENT_SECURITY_POLICY).getValue());
assertEquals("none", response.getFirstHeader(BrowserSecurityHeaders.X_ROBOTS_TAG).getValue());
assertNull(response.getFirstHeader(BrowserSecurityHeaders.X_FRAME_OPTIONS.getHeaderName()));
assertEquals("frame-src 'self'; object-src 'none';", response.getFirstHeader(BrowserSecurityHeaders.CONTENT_SECURITY_POLICY.getHeaderName()).getValue());
assertEquals("none", response.getFirstHeader(BrowserSecurityHeaders.X_ROBOTS_TAG.getHeaderName()).getValue());
response.close();

View file

@ -255,7 +255,7 @@ public class DefaultHostnameTest extends AbstractHostnameTest {
assertTrue(indexPage.contains("consoleBaseUrl = '" + new URI(expectedAdminUrl).getPath() +"/admin/" + realm + "/console/'"));
assertTrue(indexPage.contains("resourceUrl = '" + new URI(expectedAdminUrl).getPath() +"/resources/"));
String cspHeader = response.getFirstHeader(BrowserSecurityHeaders.CONTENT_SECURITY_POLICY);
String cspHeader = response.getFirstHeader(BrowserSecurityHeaders.CONTENT_SECURITY_POLICY.getHeaderName());
if (expectedFrontendUrl.equalsIgnoreCase(expectedAdminUrl)) {
assertEquals("frame-src 'self'; frame-ancestors 'self'; object-src 'none';", cspHeader);