개발

HSTS 오류 - Strict-Transport-Security 헤더에 오류가 감지됨

개미v 2022. 8. 6. 19:46

 

보안취약점으로 Strict-Transport-Security 헤더에 오류에 대한 해결방법입니다.

일반적인 경우에는 이 문제가 나오지 않을테지만, 스프링시큐리티 사용하는 경우에 발생 합니다.

원인

HTTP 헤더를 확인해보면 HSTS 헤더값에 preload가 빠졌습니다.

preload까지 들어가야 표준에 맞나 봅니다.

정상

Strict-Transport-Security: max-age=31536000; includeSubDomains; preload

오류

Strict-Transport-Security: max-age=31536000; includeSubDomains

해결

스프링시큐리티 소스인 HstsHeaderWriter.java 에 빠뜨린 preload 값을 넣어줬습니다.

그리고 HstsHeaderWriter.java 파일을 해당 패키지 경로에 위치 시키면, 프로젝트는 이 소스를 참조하게 됩니다.

※ 스프링시큐리티 사용하면 HSTS 헤더값을 스프링시큐리티에서 생성해주니, 톰캣 설정 아무리 만져봐도 안됩니다.

 

package org.springframework.security.web.header.writers;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.security.web.header.HeaderWriter;
import org.springframework.security.web.util.matcher.RequestMatcher;
import org.springframework.util.Assert;

public final class HstsHeaderWriter implements HeaderWriter {
	private static final long DEFAULT_MAX_AGE_SECONDS = 31536000L;
	private static final String HSTS_HEADER_NAME = "Strict-Transport-Security";
	private final Log logger = LogFactory.getLog(getClass());
	private RequestMatcher requestMatcher;
	private long maxAgeInSeconds;
	private boolean includeSubDomains;
	private boolean preload;
	private String hstsHeaderValue;

	public HstsHeaderWriter(RequestMatcher requestMatcher, long maxAgeInSeconds, boolean includeSubDomains, boolean preload) {
		this.requestMatcher = requestMatcher;
		this.maxAgeInSeconds = maxAgeInSeconds;
		this.includeSubDomains = includeSubDomains;
		this.preload = preload;
		updateHstsHeaderValue();
	}

	public HstsHeaderWriter(long maxAgeInSeconds, boolean includeSubDomains, boolean preload) {
		this(new SecureRequestMatcher(), maxAgeInSeconds, includeSubDomains, preload);
	}
	
	public HstsHeaderWriter(long maxAgeInSeconds, boolean includeSubDomains) {
		this(new SecureRequestMatcher(), maxAgeInSeconds, includeSubDomains, true);
	}

	public HstsHeaderWriter(long maxAgeInSeconds) {
		this(new SecureRequestMatcher(), maxAgeInSeconds, true, true);
	}

	public HstsHeaderWriter(boolean includeSubDomains) {
		this(new SecureRequestMatcher(), DEFAULT_MAX_AGE_SECONDS, includeSubDomains, true);
	}

	public HstsHeaderWriter() {
		this(DEFAULT_MAX_AGE_SECONDS);
	}

	public void writeHeaders(HttpServletRequest request, HttpServletResponse response) {
		if (this.requestMatcher.matches(request)) {
			response.setHeader(HSTS_HEADER_NAME, this.hstsHeaderValue);
		} else if (this.logger.isDebugEnabled()) {
			this.logger.debug("Not injecting HSTS header since it did not match the requestMatcher " + this.requestMatcher);
		}
	}

	public void setRequestMatcher(RequestMatcher requestMatcher) {
		Assert.notNull(requestMatcher, "requestMatcher cannot be null");
		this.requestMatcher = requestMatcher;
	}

	public void setMaxAgeInSeconds(long maxAgeInSeconds) {
		if (maxAgeInSeconds < 0L) {
			throw new IllegalArgumentException("maxAgeInSeconds must be non-negative. Got " + maxAgeInSeconds);
		}

		this.maxAgeInSeconds = maxAgeInSeconds;
		updateHstsHeaderValue();
	}

	public void setIncludeSubDomains(boolean includeSubDomains) {
		this.includeSubDomains = includeSubDomains;
		updateHstsHeaderValue();
	}
	
	public void setPreload(boolean preload) {
		this.preload = preload;
		updateHstsHeaderValue();
	}
	
	private void updateHstsHeaderValue() {
		String headerValue = "max-age=" + this.maxAgeInSeconds;
		if (this.includeSubDomains) {
			headerValue = headerValue + "; includeSubDomains";
		}
		if (this.preload) {
			headerValue += "; preload";
		}
		this.hstsHeaderValue = headerValue;
	}

	private static final class SecureRequestMatcher implements RequestMatcher {
		public boolean matches(HttpServletRequest request) {
			return request.isSecure();
		}
	}
}