Skip to content

Commit

Permalink
Feat/security header properties (#2620)
Browse files Browse the repository at this point in the history
* Initial security headers logic

* update filter

* Clean up

* move filters to module

* default security features to false

* Update defaults
  • Loading branch information
loulou2u authored Jan 13, 2023
1 parent 9e3c3b7 commit fc0b8ef
Show file tree
Hide file tree
Showing 9 changed files with 312 additions and 0 deletions.
1 change: 1 addition & 0 deletions settings.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ include 'uPortal-security:uPortal-security-mvc'
include 'uPortal-security:uPortal-security-permissions'
include 'uPortal-security:uPortal-security-services'
include 'uPortal-security:uPortal-security-xslt'
include 'uPortal-security:uPortal-security-filters'

include 'uPortal-soffit:uPortal-soffit-core'
include 'uPortal-soffit:uPortal-soffit-connector'
Expand Down
16 changes: 16 additions & 0 deletions uPortal-security/uPortal-security-filters/build.gradle
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
description = "Apereo uPortal Security Filters"

dependencies {
compile project(':uPortal-core')
compile project(':uPortal-events')

testCompile "${servletApiDependency}"

compileOnly "${servletApiDependency}"
compileOnly "org.springframework:spring-web:${springVersion}"
compileOnly "org.springframework:spring-test:${springVersion}"

compile "org.slf4j:slf4j-api:${slf4jVersion}"
compileOnly "org.projectlombok:lombok:${lombokVersion}"
annotationProcessor "org.projectlombok:lombok:${lombokVersion}"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,235 @@
package org.apereo.portal.security.filter;

import java.io.IOException;
import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang.StringUtils;

@Slf4j
@Data
public class WebSecurityFilter implements Filter {

private FilterConfig config;

// Parameters
public static final String PARAM_HSTS_INCLUDE_SUBDOMAINS = "sec.hsts.include.subdomains";
public static final String PARAM_HSTS_PRELOAD = "sec.hsts.preload";
public static final String PARAM_HSTS_ENABLED = "sec.hsts.enabled";
public static final String PARAM_HSTS_MAXAGE_SECONDS = "sec.hsts.maxage.seconds";
public static final String PARAM_ANTI_JACK_CLICKING_ENABLED = "sec.anti.click.jacking.enabled";
public static final String PARAM_ANTI_JACK_CLICKING_OPTIONS = "sec.anti.click.jacking.options";
public static final String PARAM_ANTI_JACK_CLICKING_URI = "sec.anti.click.jacking.uri";
public static final String PARAM_X_CONTENT_TYPE_ENABLED = "sec.x.content.type.enabled";
public static final String PARAM_CONTENT_SECURITY_POLICY_ENABLED =
"sec.content.sec.policy.enabled";
public static final String PARAM_CONTENT_SECURITY_POLICY = "sec.content.sec.policy";
public static final String PARAM_REFERRER_POLICY_ENABLED = "sec.referrer.policy.enabled";
public static final String PARAM_REFERRER_POLICY = "sec.referrer.policy";

// Default values
public static final String DEFAULT_HSTS_HEADER_NAME = "Strict-Transport-Security";
public static final String DEFAULT_HSTS_ENABLED = "false";
public static final String DEFAULT_HSTS_MAXAGE_SECONDS = "0";
public static final String DEFAULT_HSTS_INCLUDE_SUBDOMAINS = "false";
public static final String DEFAULT_HSTS_PRELOAD = "false";
public static final String DEFAULT_ANTI_JACK_CLICKING_HEADER = "X-Frame-Options";
public static final String DEFAULT_ANTI_JACK_CLICKING_ENABLED = "true";
public static final String DEFAULT_ANTI_JACK_CLICKING_OPTIONS = "deny,sameorigin,allow-from";
public static final String DEFAULT_ANTI_JACK_CLICKING_URI = "";
public static final String DEFAULT_X_CONTENT_TYPE_HEADER = "X-Content-Type-Options";
public static final String DEFAULT_X_CONTENT_TYPE_ENABLED = "true";
public static final String DEFAULT_CONTENT_SECURITY_POLICY_HEADER = "Content-Security-Policy";
public static final String DEFAULT_CONTENT_SECURITY_POLICY_ENABLED = "false";
public static final String DEFAULT_CONTENT_SECURITY_POLICY = "";
public static final String DEFAULT_REFERRER_POLICY_ENABLED = "true";
public static final String DEFAULT_REFERRER_POLICY = "no-referrer";

private boolean hstsEnabled;
private long hstsMaxAgeSeconds;
private boolean hstsIncludeSubDomains;
private boolean hstsPreload;
private boolean antiJackClickingEnabled;
private String antiJackClickingOptions;
private String antiJackClickingUri;
private boolean xContentTypeEnabled;
private boolean contentSecurityPolicyEnabled;
private String contentSecurityPolicy;
private boolean referrerPolicyEnabled;
private String referrerPolicy;

public void setFilterConfig(FilterConfig config) {
this.config = config;
}

public FilterConfig getFilterConfig() {
return config;
}

public WebSecurityFilter() throws ServletException {
parseAndStore(
DEFAULT_HSTS_ENABLED,
DEFAULT_HSTS_MAXAGE_SECONDS,
DEFAULT_HSTS_INCLUDE_SUBDOMAINS,
DEFAULT_HSTS_PRELOAD,
DEFAULT_ANTI_JACK_CLICKING_ENABLED,
DEFAULT_ANTI_JACK_CLICKING_OPTIONS,
DEFAULT_ANTI_JACK_CLICKING_URI,
DEFAULT_X_CONTENT_TYPE_ENABLED,
DEFAULT_CONTENT_SECURITY_POLICY_ENABLED,
DEFAULT_CONTENT_SECURITY_POLICY,
DEFAULT_REFERRER_POLICY_ENABLED,
DEFAULT_REFERRER_POLICY);
}

@Override
public void doFilter(
ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain)
throws IOException, ServletException {

if (!(servletRequest instanceof HttpServletRequest)
|| !(servletResponse instanceof HttpServletResponse)) {
throw new ServletException("WebSecurity doesn't support non-HTTP request or response");
}

HttpServletRequest request = (HttpServletRequest) servletRequest;
HttpServletResponse response = (HttpServletResponse) servletResponse;

if (hstsEnabled) {
String declarations = null;
if (hstsMaxAgeSeconds > 0) {
declarations = "max-age=" + hstsMaxAgeSeconds + ";";
}
if (hstsIncludeSubDomains) {
declarations += " includeSubdomains;";
}
if (hstsPreload) {
declarations += " preload;";
}
response.setHeader(
WebSecurityFilter.DEFAULT_HSTS_HEADER_NAME, StringUtils.chop(declarations));
}

if (antiJackClickingEnabled) {
String options = null;
if (!antiJackClickingOptions.equals("allow-from")) {
options = antiJackClickingOptions.toUpperCase();
} else if (!antiJackClickingUri.isEmpty()) {
options = antiJackClickingOptions.toUpperCase() + "'" + antiJackClickingUri + "'";
}
response.setHeader(DEFAULT_ANTI_JACK_CLICKING_HEADER, options);
log.debug("Testing options for anti jack clicking: " + options);
}

if (contentSecurityPolicyEnabled) {
response.setHeader(DEFAULT_CONTENT_SECURITY_POLICY_HEADER, contentSecurityPolicy);
log.debug("Content-Security-Policy: " + contentSecurityPolicy);
}

if (xContentTypeEnabled) {
log.debug("Content Type Options: " + response.getHeader("Content-Type-Options"));
response.setHeader(DEFAULT_X_CONTENT_TYPE_HEADER, "nosniff");
}

if (referrerPolicyEnabled) {
log.debug("Referrer Policy: " + referrerPolicy);
response.setHeader("Referrer-Policy", referrerPolicy);
}

filterChain.doFilter(request, response);
}

private void parseAndStore(
final String hstsEnabled,
final String hstsMaxAgeSeconds,
final String hstsIncludeSubDomains,
final String hstsPreload,
final String antiJackClickingEnabled,
final String antiJackClickingOptions,
final String antiJackClickingUri,
final String xContentTypeEnabled,
final String contentSecurityPolicyEnabled,
final String contentSecurityPolicy,
final String referrerPolicyEnabled,
final String referrerPolicy)
throws ServletException {

setHstsEnabled(Boolean.parseBoolean(hstsEnabled));
setHstsMaxAgeSeconds(hstsMaxAgeSeconds);
setHstsIncludeSubDomains(Boolean.parseBoolean(hstsIncludeSubDomains));
setHstsPreload(Boolean.parseBoolean(hstsPreload));
setAntiJackClickingEnabled(Boolean.parseBoolean(antiJackClickingEnabled));
setAntiJackClickingOptions(antiJackClickingOptions);
setAntiJackClickingUri(antiJackClickingUri);
setXContentTypeEnabled(Boolean.parseBoolean(xContentTypeEnabled));
setContentSecurityPolicyEnabled(Boolean.parseBoolean(contentSecurityPolicyEnabled));
setContentSecurityPolicy(contentSecurityPolicy);
setReferrerPolicyEnabled(Boolean.parseBoolean(referrerPolicyEnabled));
setReferrerPolicy(referrerPolicy);
}

public void setHstsMaxAgeSeconds(String hstsMaxAgeSeconds) throws ServletException {
log.debug("setHstsMaxAgeSeconds set to {}", hstsMaxAgeSeconds);
try {
if (!hstsMaxAgeSeconds.isEmpty()) {
this.hstsMaxAgeSeconds = Long.parseLong(hstsMaxAgeSeconds);
} else {
this.hstsMaxAgeSeconds = 0L;
}
} catch (NumberFormatException e) {
throw new ServletException("Unable to parse hstsMaxAgeSeconds", e);
}
}

@Override
public void init(FilterConfig config) throws ServletException {
setFilterConfig(config);
init();
}

public void init() throws ServletException {
parseAndStore(
getInitParameter(PARAM_HSTS_ENABLED, DEFAULT_HSTS_ENABLED),
getInitParameter(PARAM_HSTS_MAXAGE_SECONDS, DEFAULT_HSTS_MAXAGE_SECONDS),
getInitParameter(PARAM_HSTS_INCLUDE_SUBDOMAINS, DEFAULT_HSTS_INCLUDE_SUBDOMAINS),
getInitParameter(PARAM_HSTS_PRELOAD, DEFAULT_HSTS_PRELOAD),
getInitParameter(
PARAM_ANTI_JACK_CLICKING_ENABLED, DEFAULT_ANTI_JACK_CLICKING_ENABLED),
getInitParameter(
PARAM_ANTI_JACK_CLICKING_OPTIONS, DEFAULT_ANTI_JACK_CLICKING_OPTIONS),
getInitParameter(PARAM_ANTI_JACK_CLICKING_URI, DEFAULT_ANTI_JACK_CLICKING_URI),
getInitParameter(PARAM_X_CONTENT_TYPE_ENABLED, DEFAULT_X_CONTENT_TYPE_ENABLED),
getInitParameter(
PARAM_CONTENT_SECURITY_POLICY_ENABLED,
DEFAULT_CONTENT_SECURITY_POLICY_ENABLED),
getInitParameter(PARAM_CONTENT_SECURITY_POLICY, DEFAULT_CONTENT_SECURITY_POLICY),
getInitParameter(PARAM_REFERRER_POLICY_ENABLED, DEFAULT_REFERRER_POLICY_ENABLED),
getInitParameter(PARAM_REFERRER_POLICY, DEFAULT_REFERRER_POLICY));
}

public String getInitParameter(String name) {
FilterConfig fc = getFilterConfig();
if (fc == null) {
throw new IllegalStateException("FilterConfig not initialized");
}
return fc.getInitParameter(name);
}

private String getInitParameter(String name, String defaultValue) {
String value = getInitParameter(name);
if (value != null) {
return value;
}
return defaultValue;
}

@Override
public void destroy() {}
}
1 change: 1 addition & 0 deletions uPortal-webapp/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ dependencies {
compile project(':uPortal-portlets')
compile project(':uPortal-security:uPortal-security-authn')
compile project(':uPortal-security:uPortal-security-xslt')
compile project(':uPortal-security:uPortal-security-filters')
compile project(':uPortal-soffit:uPortal-soffit-connector')
compile project(':uPortal-utils:uPortal-utils-jmx')
compile project(':uPortal-utils:uPortal-utils-url')
Expand Down
4 changes: 4 additions & 0 deletions uPortal-webapp/src/main/resources/logback.xml
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,10 @@
<appender-ref ref="TINCAN"/>
</logger>

<logger name="org.apereo.portal.security.filter" additivity="false" level="DEBUG">
<appender-ref ref="PORTAL"/>
</logger>

<!-- Debug CAS Clearpass
<logger name="org.apereo.portal.security.provider.cas" additivity="false" level="TRACE">
<appender-ref ref="PORTAL"/>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,21 @@
<property name="decorateRequest" value="${cors.request.decorate:true}" />
</bean>

<bean id="webSecurityFilter" class="org.apereo.portal.security.filter.WebSecurityFilter">
<property name="hstsEnabled" value="${sec.hsts.enabled:false}" />
<property name="hstsMaxAgeSeconds" value="${sec.hsts.maxage.seconds:0}" />
<property name="hstsIncludeSubDomains" value="${sec.hsts.include.subdomains:false}" />
<property name="hstsPreload" value="${sec.hsts.preload:false}" />
<property name="antiJackClickingEnabled" value="${sec.anti.click.jacking.enabled:false}" />
<property name="antiJackClickingOptions" value="${sec.anti.click.jacking.options:}" />
<property name="antiJackClickingUri" value="${sec.anti.click.jacking.uri:}" />
<property name="xContentTypeEnabled" value="${sec.x.content.type.enabled:false}" />
<property name="contentSecurityPolicyEnabled" value="${sec.content.sec.policy.enabled:false}" />
<property name="contentSecurityPolicy" value="${sec.content.sec.policy:}" />
<property name="referrerPolicyEnabled" value="${sec.referrer.policy.enabled:false}" />
<property name="referrerPolicy" value="${sec.referrer.policy:}" />
</bean>

<!--
| Spring-managed instance of EhcacheBackedProxyGrantingTicketStorageImpl. This setup allows us
| to configure CAS authentication in the preferred approach. This bean is needed to share the proxyGrantingTicket
Expand Down
30 changes: 30 additions & 0 deletions uPortal-webapp/src/main/resources/properties/security.properties
Original file line number Diff line number Diff line change
Expand Up @@ -132,3 +132,33 @@ org.apereo.portal.channels.CLogin.CasLoginUrl=${cas.protocol.server.context}/log
#cors.support.credentials=true
#cors.preflight.maxage=1800
#cors.request.decorate=true

##
## Tomcat HTTP Security Headers
##

# antiClickJackingEnabled: X-Frame-Options header
sec.anti.click.jacking.enabled=false
# X-Frame-Options: deny, sameorigin, allow-from
sec.anti.click.jacking.options=sameorigin
# If allow-from is selected above, add URI
sec.anti.click.jacking.uri=

# Content-Security-Policy: default-src, script-src, style-src, img-src
# See more details at: https://content-security-policy.com/
sec.content.sec.policy.enabled=false
sec.content.sec.policy=default-src 'self'

# Strict-Transport-Security: max-age=###; includeSubDomains; preload
sec.hsts.enabled=false
sec.hsts.maxage.seconds=31536000
sec.hsts.include.subdomains=true
sec.hsts.preload=false

# X-Content-Type-Options: "nosniff" will be used if enabled is set to true
sec.x.content.type.enabled=false

# Referrer-Policy available directives to pass include:
# See more details at: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Referrer-Policy
sec.referrer.policy.enabled=false
sec.referrer.policy=no-referrer
10 changes: 10 additions & 0 deletions uPortal-webapp/src/main/webapp/WEB-INF/web.xml
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,11 @@
<filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
</filter>

<filter>
<filter-name>webSecurityFilter</filter-name>
<filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
</filter>

<filter>
<filter-name>requestParameterFilter</filter-name>
<filter-class>org.apereo.portal.security.firewall.RequestParameterPolicyEnforcementFilter</filter-class>
Expand Down Expand Up @@ -239,6 +244,11 @@
<url-pattern>/*</url-pattern>
</filter-mapping>

<filter-mapping>
<filter-name>webSecurityFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>

<filter-mapping>
<filter-name>CAS Authentication Filter</filter-name>
<url-pattern>/Login</url-pattern>
Expand Down

0 comments on commit fc0b8ef

Please sign in to comment.