From 7342261dbea4011f15541e46125110cd4f12afed Mon Sep 17 00:00:00 2001 From: Stian Thorgersen Date: Fri, 11 Mar 2016 15:25:21 +0100 Subject: [PATCH] KEYCLOAK-2593 Character set missing from responses and no content sniffing defense in place --- .../keycloak/migration/MigrationModel.java | 2 +- .../migration/MigrationModelManager.java | 7 ++++ .../migration/migrators/MigrateTo1_9_2.java | 37 +++++++++++++++++++ .../models/BrowserSecurityHeaders.java | 6 ++- .../freemarker/FreeMarkerAccountProvider.java | 4 +- .../FreeMarkerLoginFormsProvider.java | 6 +-- .../services/resources/WelcomeResource.java | 26 +++++++++---- .../resources/admin/AdminConsole.java | 5 +-- .../theme/BrowserSecurityHeaderSetup.java | 5 ++- .../java/org/keycloak/utils/MediaType.java | 34 +++++++++++++++++ .../messages/admin-messages_en.properties | 5 ++- .../resources/partials/defense-headers.html | 15 ++++++-- 12 files changed, 126 insertions(+), 26 deletions(-) create mode 100644 server-spi/src/main/java/org/keycloak/migration/migrators/MigrateTo1_9_2.java create mode 100644 services/src/main/java/org/keycloak/utils/MediaType.java diff --git a/server-spi/src/main/java/org/keycloak/migration/MigrationModel.java b/server-spi/src/main/java/org/keycloak/migration/MigrationModel.java index ec8137ea80..6196a2903c 100755 --- a/server-spi/src/main/java/org/keycloak/migration/MigrationModel.java +++ b/server-spi/src/main/java/org/keycloak/migration/MigrationModel.java @@ -26,7 +26,7 @@ public interface MigrationModel { /** * Must have the form of major.minor.micro as the version is parsed and numbers are compared */ - String LATEST_VERSION = "1.9.0"; + String LATEST_VERSION = "1.9.2"; String getStoredVersion(); void setStoredVersion(String version); diff --git a/server-spi/src/main/java/org/keycloak/migration/MigrationModelManager.java b/server-spi/src/main/java/org/keycloak/migration/MigrationModelManager.java index 9b1442c557..7a29804b9b 100755 --- a/server-spi/src/main/java/org/keycloak/migration/MigrationModelManager.java +++ b/server-spi/src/main/java/org/keycloak/migration/MigrationModelManager.java @@ -25,6 +25,7 @@ import org.keycloak.migration.migrators.MigrateTo1_6_0; import org.keycloak.migration.migrators.MigrateTo1_7_0; import org.keycloak.migration.migrators.MigrateTo1_8_0; import org.keycloak.migration.migrators.MigrateTo1_9_0; +import org.keycloak.migration.migrators.MigrateTo1_9_2; import org.keycloak.migration.migrators.MigrationTo1_2_0_CR1; import org.keycloak.models.KeycloakSession; @@ -92,6 +93,12 @@ public class MigrationModelManager { } new MigrateTo1_9_0().migrate(session); } + if (stored == null || stored.lessThan(MigrateTo1_9_2.VERSION)) { + if (stored != null) { + logger.debug("Migrating older model to 1.9.2 updates"); + } + new MigrateTo1_9_2().migrate(session); + } model.setStoredVersion(MigrationModel.LATEST_VERSION); } diff --git a/server-spi/src/main/java/org/keycloak/migration/migrators/MigrateTo1_9_2.java b/server-spi/src/main/java/org/keycloak/migration/migrators/MigrateTo1_9_2.java new file mode 100644 index 0000000000..7c1f09722b --- /dev/null +++ b/server-spi/src/main/java/org/keycloak/migration/migrators/MigrateTo1_9_2.java @@ -0,0 +1,37 @@ +/* + * Copyright 2016 Red Hat, Inc. and/or its affiliates + * and other contributors as indicated by the @author tags. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.keycloak.migration.migrators; + +import org.keycloak.migration.ModelVersion; +import org.keycloak.models.BrowserSecurityHeaders; +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.RealmModel; + +public class MigrateTo1_9_2 { + + public static final ModelVersion VERSION = new ModelVersion("1.9.2"); + + public void migrate(KeycloakSession session) { + for (RealmModel realm : session.realms().getRealms()) { + if (realm.getBrowserSecurityHeaders() != null) { + realm.getBrowserSecurityHeaders().put("xFrameOptions", "nosniff"); + } + } + } + +} diff --git a/server-spi/src/main/java/org/keycloak/models/BrowserSecurityHeaders.java b/server-spi/src/main/java/org/keycloak/models/BrowserSecurityHeaders.java index 4abc494eae..10560271b0 100755 --- a/server-spi/src/main/java/org/keycloak/models/BrowserSecurityHeaders.java +++ b/server-spi/src/main/java/org/keycloak/models/BrowserSecurityHeaders.java @@ -30,13 +30,15 @@ public class BrowserSecurityHeaders { public static final Map defaultHeaders; static { - Map headerMap = new HashMap(); + Map headerMap = new HashMap<>(); headerMap.put("xFrameOptions", "X-Frame-Options"); headerMap.put("contentSecurityPolicy", "Content-Security-Policy"); + headerMap.put("xContentTypeOptions", "X-Content-Type-Options"); - Map dh = new HashMap(); + Map dh = new HashMap<>(); dh.put("xFrameOptions", "SAMEORIGIN"); dh.put("contentSecurityPolicy", "frame-src 'self'"); + dh.put("xContentTypeOptions", "nosniff"); defaultHeaders = Collections.unmodifiableMap(dh); headerAttributeMap = Collections.unmodifiableMap(headerMap); diff --git a/services/src/main/java/org/keycloak/forms/account/freemarker/FreeMarkerAccountProvider.java b/services/src/main/java/org/keycloak/forms/account/freemarker/FreeMarkerAccountProvider.java index fd701245af..893e816592 100755 --- a/services/src/main/java/org/keycloak/forms/account/freemarker/FreeMarkerAccountProvider.java +++ b/services/src/main/java/org/keycloak/forms/account/freemarker/FreeMarkerAccountProvider.java @@ -27,7 +27,6 @@ import java.util.Map; import java.util.Properties; import javax.ws.rs.core.HttpHeaders; -import javax.ws.rs.core.MediaType; import javax.ws.rs.core.MultivaluedMap; import javax.ws.rs.core.Response; import javax.ws.rs.core.UriBuilder; @@ -64,6 +63,7 @@ import org.keycloak.models.RealmModel; import org.keycloak.models.UserModel; import org.keycloak.models.UserSessionModel; import org.keycloak.models.utils.FormMessage; +import org.keycloak.utils.MediaType; /** * @author Stian Thorgersen @@ -209,7 +209,7 @@ public class FreeMarkerAccountProvider implements AccountProvider { try { String result = freeMarker.processTemplate(attributes, Templates.getTemplate(page), theme); - Response.ResponseBuilder builder = Response.status(status).type(MediaType.TEXT_HTML).entity(result); + Response.ResponseBuilder builder = Response.status(status).type(MediaType.TEXT_HTML_UTF_8_TYPE).language(locale).entity(result); BrowserSecurityHeaderSetup.headers(builder, realm); return builder.build(); } catch (FreeMarkerException e) { diff --git a/services/src/main/java/org/keycloak/forms/login/freemarker/FreeMarkerLoginFormsProvider.java b/services/src/main/java/org/keycloak/forms/login/freemarker/FreeMarkerLoginFormsProvider.java index bb2e518d9c..1a81870ee0 100755 --- a/services/src/main/java/org/keycloak/forms/login/freemarker/FreeMarkerLoginFormsProvider.java +++ b/services/src/main/java/org/keycloak/forms/login/freemarker/FreeMarkerLoginFormsProvider.java @@ -61,8 +61,8 @@ import org.keycloak.models.UserModel; import org.keycloak.models.utils.FormMessage; import org.keycloak.services.Urls; import org.keycloak.services.messages.Messages; +import org.keycloak.utils.MediaType; -import javax.ws.rs.core.MediaType; import javax.ws.rs.core.MultivaluedMap; import javax.ws.rs.core.Response; import javax.ws.rs.core.UriBuilder; @@ -312,7 +312,7 @@ public class FreeMarkerLoginFormsProvider implements LoginFormsProvider { try { String result = freeMarker.processTemplate(attributes, Templates.getTemplate(page), theme); - Response.ResponseBuilder builder = Response.status(status).type(MediaType.TEXT_HTML).entity(result); + Response.ResponseBuilder builder = Response.status(status).type(MediaType.TEXT_HTML_UTF_8).entity(result); BrowserSecurityHeaderSetup.headers(builder, realm); for (Map.Entry entry : httpResponseHeaders.entrySet()) { builder.header(entry.getKey(), entry.getValue()); @@ -413,7 +413,7 @@ public class FreeMarkerLoginFormsProvider implements LoginFormsProvider { } try { String result = freeMarker.processTemplate(attributes, form, theme); - Response.ResponseBuilder builder = Response.status(status).type(MediaType.TEXT_HTML).entity(result); + Response.ResponseBuilder builder = Response.status(status).type(MediaType.TEXT_HTML_UTF_8_TYPE).language(locale).entity(result); BrowserSecurityHeaderSetup.headers(builder, realm); for (Map.Entry entry : httpResponseHeaders.entrySet()) { builder.header(entry.getKey(), entry.getValue()); diff --git a/services/src/main/java/org/keycloak/services/resources/WelcomeResource.java b/services/src/main/java/org/keycloak/services/resources/WelcomeResource.java index e2ee4025f1..23e9baa636 100755 --- a/services/src/main/java/org/keycloak/services/resources/WelcomeResource.java +++ b/services/src/main/java/org/keycloak/services/resources/WelcomeResource.java @@ -17,17 +17,27 @@ package org.keycloak.services.resources; import org.keycloak.Config; -import org.keycloak.theme.FreeMarkerUtil; -import org.keycloak.theme.Theme; -import org.keycloak.theme.ThemeProvider; -import org.keycloak.models.KeycloakSession; import org.keycloak.common.util.MimeTypeUtil; +import org.keycloak.models.KeycloakSession; import org.keycloak.services.ServicesLogger; import org.keycloak.services.managers.ApplianceBootstrap; import org.keycloak.services.util.CacheControlUtil; +import org.keycloak.theme.FreeMarkerUtil; +import org.keycloak.theme.Theme; +import org.keycloak.theme.ThemeProvider; +import org.keycloak.utils.MediaType; -import javax.ws.rs.*; -import javax.ws.rs.core.*; +import javax.ws.rs.Consumes; +import javax.ws.rs.GET; +import javax.ws.rs.POST; +import javax.ws.rs.Path; +import javax.ws.rs.PathParam; +import javax.ws.rs.Produces; +import javax.ws.rs.WebApplicationException; +import javax.ws.rs.core.Context; +import javax.ws.rs.core.MultivaluedMap; +import javax.ws.rs.core.Response; +import javax.ws.rs.core.UriInfo; import java.io.IOException; import java.io.InputStream; import java.net.InetAddress; @@ -64,7 +74,7 @@ public class WelcomeResource { * @throws URISyntaxException */ @GET - @Produces("text/html") + @Produces(MediaType.TEXT_HTML_UTF_8) public Response getWelcomePage() throws URISyntaxException { checkBootstrap(); @@ -127,7 +137,7 @@ public class WelcomeResource { */ @GET @Path("/welcome-content/{path}") - @Produces("text/html") + @Produces(MediaType.TEXT_HTML_UTF_8) public Response getResource(@PathParam("path") String path) { try { InputStream resource = getTheme().getResourceAsStream(path); diff --git a/services/src/main/java/org/keycloak/services/resources/admin/AdminConsole.java b/services/src/main/java/org/keycloak/services/resources/admin/AdminConsole.java index ba7c83af78..9ed474b5c7 100644 --- a/services/src/main/java/org/keycloak/services/resources/admin/AdminConsole.java +++ b/services/src/main/java/org/keycloak/services/resources/admin/AdminConsole.java @@ -42,14 +42,13 @@ import org.keycloak.services.managers.AuthenticationManager; import org.keycloak.services.managers.RealmManager; import org.keycloak.services.resources.KeycloakApplication; import org.keycloak.services.Urls; +import org.keycloak.utils.MediaType; import javax.ws.rs.GET; import javax.ws.rs.Path; import javax.ws.rs.Produces; -import javax.ws.rs.WebApplicationException; import javax.ws.rs.core.Context; import javax.ws.rs.core.HttpHeaders; -import javax.ws.rs.core.MediaType; import javax.ws.rs.core.Response; import javax.ws.rs.core.UriInfo; import javax.ws.rs.ext.Providers; @@ -296,7 +295,7 @@ public class AdminConsole { FreeMarkerUtil freeMarkerUtil = new FreeMarkerUtil(); String result = freeMarkerUtil.processTemplate(map, "index.ftl", theme); - Response.ResponseBuilder builder = Response.status(Response.Status.OK).type(MediaType.TEXT_HTML).entity(result); + Response.ResponseBuilder builder = Response.status(Response.Status.OK).type(MediaType.TEXT_HTML_UTF_8).language(Locale.ENGLISH).entity(result); BrowserSecurityHeaderSetup.headers(builder, realm); return builder.build(); } diff --git a/services/src/main/java/org/keycloak/theme/BrowserSecurityHeaderSetup.java b/services/src/main/java/org/keycloak/theme/BrowserSecurityHeaderSetup.java index 41fc584529..dfcbf50e1d 100755 --- a/services/src/main/java/org/keycloak/theme/BrowserSecurityHeaderSetup.java +++ b/services/src/main/java/org/keycloak/theme/BrowserSecurityHeaderSetup.java @@ -32,8 +32,9 @@ public class BrowserSecurityHeaderSetup { public static Response.ResponseBuilder headers(Response.ResponseBuilder builder, RealmModel realm) { for (Map.Entry entry : realm.getBrowserSecurityHeaders().entrySet()) { String headerName = BrowserSecurityHeaders.headerAttributeMap.get(entry.getKey()); - if (headerName == null) continue; - builder.header(headerName, entry.getValue()); + if (headerName != null && entry.getValue() != null && entry.getValue().length() > 0) { + builder.header(headerName, entry.getValue()); + } } return builder; } diff --git a/services/src/main/java/org/keycloak/utils/MediaType.java b/services/src/main/java/org/keycloak/utils/MediaType.java new file mode 100644 index 0000000000..31ab972392 --- /dev/null +++ b/services/src/main/java/org/keycloak/utils/MediaType.java @@ -0,0 +1,34 @@ +/* + * Copyright 2016 Red Hat, Inc. and/or its affiliates + * and other contributors as indicated by the @author tags. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.keycloak.utils; + +/** + * @author Stian Thorgersen + */ +public class MediaType { + + public static final String TEXT_HTML_UTF_8 = "text/html; charset=utf-8"; + public static final javax.ws.rs.core.MediaType TEXT_HTML_UTF_8_TYPE = new javax.ws.rs.core.MediaType("text", "html", "utf-8"); + + public static final String APPLICATION_JSON = javax.ws.rs.core.MediaType.APPLICATION_JSON; + public static final javax.ws.rs.core.MediaType APPLICATION_JSON_TYPE = javax.ws.rs.core.MediaType.APPLICATION_JSON_TYPE; + + public static final String APPLICATION_FORM_URLENCODED = javax.ws.rs.core.MediaType.APPLICATION_FORM_URLENCODED; + public static final javax.ws.rs.core.MediaType APPLICATION_FORM_URLENCODED_TYPE = javax.ws.rs.core.MediaType.APPLICATION_FORM_URLENCODED_TYPE; + +} diff --git a/themes/src/main/resources/theme/base/admin/messages/admin-messages_en.properties b/themes/src/main/resources/theme/base/admin/messages/admin-messages_en.properties index 0a0cfc58fe..a86792abec 100644 --- a/themes/src/main/resources/theme/base/admin/messages/admin-messages_en.properties +++ b/themes/src/main/resources/theme/base/admin/messages/admin-messages_en.properties @@ -95,8 +95,11 @@ login-action-timeout.tooltip=Max time a user has to complete login related actio headers=Headers brute-force-detection=Brute Force Detection x-frame-options=X-Frame-Options -click-label-for-info=Click on label link for more information. The default value prevents pages from being included via non-origin iframes. +x-frame-options-tooltip=Default value prevents pages from being included via non-origin iframes (click label for more information) content-sec-policy=Content-Security-Policy +content-sec-policy-tooltip=Default value prevents pages from being included via non-origin iframes (click label for more information) +content-type-options=X-Content-Type-Options +content-type-options-tooltip=Default value prevents Internet Explorer and Google Chrome from MIME-sniffing a response away from the declared content-type (click label for more information) max-login-failures=Max Login Failures max-login-failures.tooltip=How many failures before wait is triggered. wait-increment=Wait Increment diff --git a/themes/src/main/resources/theme/base/admin/resources/partials/defense-headers.html b/themes/src/main/resources/theme/base/admin/resources/partials/defense-headers.html index aa1dc4e29f..1dc08a1663 100755 --- a/themes/src/main/resources/theme/base/admin/resources/partials/defense-headers.html +++ b/themes/src/main/resources/theme/base/admin/resources/partials/defense-headers.html @@ -9,18 +9,25 @@
- +
- {{:: 'click-label-for-info' | translate}} + {{:: 'x-frame-options-tooltip' | translate}}
- +
- {{:: 'click-label-for-info' | translate}} + {{:: 'content-sec-policy-tooltip' | translate}} +
+
+ +
+ +
+ {{:: 'content-type-options-tooltip' | translate}}