오래된 Java WAS(정확히는 Servlet Container)인 Tomcat을 안전하게 운영하기 위한 설정을 알아보자.
1. 불필요 폴더 삭제
Tomcat을 설치하면 기본 appBase 인 wepapps 폴더에 예제(example), 문서(docs) 등 여러 가지 context들이 있다.
이들 대부분은 구성에 따라 사용하기도 하지만 사용하지 않는 것들이 있다면 모두 지워야 한다.
2. 계정정보 변경
/conf/tomcat-users.xml 에 role, user 정보를 운영에 맞도록 설정한다.
wepapps에 배포(deployment) 시에 manager 등을 사용하지 않을 경우 이 부분은 설정하지 않도록 한다.
manager 등을 사용하지 않는다면 webapps에서 지워야 한다.
3. 서버정보 노출 차단
/conf/server.xml 에 Connector 태그에 server 속성을 설정해서 서버의 기본정보를 헤더에 노출시키지 않도록 한다.
* 속성을 지정하지 않을 경우 기본값인 Tomcat 정보가 노출된다.
* 속성을 명시하고 값을 주지 않을 경우 해당 정보가 노출되지 않는다.
* Connectior 태그 모두에게 줄 수 있다.
<Connector port="8080" protocol="HTTP/1.1" connectionTimeout="20000" redirectPort="8443" server=""/>
...
<Connector port="8443"
protocol="org.apache.coyote.http11.Http11Protocol" maxThreads="150"
SSLEnabled="true" scheme="https" secure="true" clientAuth="false" sslProtocol="TLS"
server="" />
...
<Connector port="8009" protocol="AJP/1.3" redirectPort="8443"
server="" />
4. 에러페이지 설정
/conf/server.xml 에 ErrorReportValve를 설정해서 에러 페이지를 설정할 수 있다.
* showReport 속성을 false로 설정해서 상세에러 내역(스택 트레이스)을 표시하지 않는다.
* showServerInfo 속성을 false로 설정해서 서버정보를 노출시키지 않는다.
<Valve className="org.apache.catalina.valves.ErrorReportValve"
showReport="false"
showServerInfo="false"/>
* errorCode.X 옵션으로 사용자 에러페이지를 기본페이지로 설정할 수 있다
.(Tomcat 9.x 이상 가능, X는 에러코드, 0일 경우 기본에러 페이지)
<Valve className="org.apache.catalina.valves.ErrorReportValve"
showReport="false"
showServerInfo="false"
errorCode.0="C:\Program Files\Apache Software Foundation\tomcat-9.0.40\webapps\error.html"/>
5. 각종 보안헤더 설정
/conf/web.xml 에 HttpHeaderSecurityFilter를 통해서 보안헤더를 설정할 수 있다.
- X-Frame-Options 설정(클릭재킹 방지)
HttpHeaderSecurityFilter의 antiClickJackingEnabled 파라미터를 true로 설정하고(기본 true),
antiClickJackingOption 값을 설정(기본 DENY) 하여 클릭재킹을 방지한다.
X-Frame-Options 은 웹 페이지가 다른 웹 페이지 내의 <frame>, <iframe>, <object> 등을 통해 로드되는 것을 방지하 기 위한 보안 헤더이다. 클릭 재킹은 공격자가 사용자의 클릭을 유도하여 실제로 의도하지 않은 행동을 수행하도록 하는 공격이다. 사용자의 클릭을 유도하여 다른 웹사이트를 iframe으로 로드하고, 그 위에 투명한 레이어를 덮어 클릭을 가로채는 등의 기법을 사용할 수 있다.
- X-Content-Type-Options 설정(MIME 타입 보안)
HttpHeaderSecurityFilter의 blockContentTypeSniffingEnabled 파라미터를 true로 설정(기본 true)하면 X-Content-Type-Options : nosniff으로 헤더를 설정하면 지정된 MIME형식 이외의 다른 용도로 사용하고자 하는 것을 차단한다.
웹서버는 HTTP 응답에 Content-Type: 헤더를 선언하여 자신이 보내는 내용이 어떠한 용도로 사용될 수 있는 지를 지정한다. 이 헤더에는 MIME(Multipurpose Internet Mail Extensions) 형식이 사용된다. (참고: https://webhack.dynu.net/?idx=20161120.001)
- HSTS(HTTP Strict Transport Security) 설정
HttpHeaderSecurityFilter의 hstsEnabled 파라미터를 true로 설정(기본 true) 하고,
hstsMaxAgeSeconds 값을 설정(음수일 경우 0으로 처리됨)하여 HSTS 보안을 설정한다.
HSTS는 http에서 https로 변환되는 취약점에 대해 보안설정을 하는 것으로,
사용자가 최초로 사이트에 접속시도를 하게 되면 웹서버는 HSTS 설정에 대한 정보를 브라우저에게 응답하게 된다.
브라우저는 이 응답을 근거로 일정시간 (max-age) 동안 HSTS 응답을 받은 웹사이트에 대해서 https 접속을 강제한다.(참고: https://rsec.kr/?p=315)
<filter>
<filter-name>httpHeaderSecurity</filter-name>
<filter-class>org.apache.catalina.filters.HttpHeaderSecurityFilter</filter-class>
<init-param>
<param-name>hstsEnabled</param-name>
<param-value>true</param-value>
</init-param>
<init-param>
<param-name>hstsMaxAgeSeconds</param-name>
<param-value>31536000</param-value>
</init-param>
<init-param>
<param-name>blockContentTypeSniffingEnabled</param-name>
<param-value>true</param-value>
</init-param>
<init-param>
<param-name>antiClickJackingEnabled</param-name>
<param-value>true</param-value>
</init-param>
<init-param>
<param-name>antiClickJackingOption</param-name>
<param-value>SAMEORIGIN</param-value>
</init-param>
</filter>
<filter-mapping>
<filter-name>httpHeaderSecurity</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
6. security-constraint 설정
/conf/web.xml 에 security-constraint 태그를 통해 보안 설정을 할 수 있다.
- 불필요한 HTTP 메서드 차단
/conf/web.xml 에 security-constraint 태그의 RestrictedMethods 리소스 설정을 통해서 불필요한 HTTP 메서드에 대해 차단할 수 있다.
불필요한 HTTP 메서드가 활성화되어 있다면 WebDAV 등을 이용하여 악용될 수 있다.
* WebDAV(Web-based Distributed Authooring and Versioning: 웹기반 분산형 저작 및 버전관리)를 말하면 웹을 통해 웹서버의 파일을 관리(조회, 수정, 삭제, 이동 등)를 할 수 있는 확장된 HTTP 프로토콜
(참고: https://m.blog.naver.com/neolims/50003141403)
- HTTPS로만 접근하도록 변경
/conf/web.xml 에 security-constraint 태그의 SecurePages 리소스 설정을 통해서 HTTPS 로만 서비스하도록 강제할 수 있다.
/conf/server.xml에 Connector 설정을 통해서 SSL 설정 시에 HTTP 가 HTTPS로 리다이렉트 되도록 설정은 하지만 도구를 이용해서 직접 접근할 경우 리다이렉트가 안되고 HTTP로 접근이 되기 때문에 네트워크 트래픽에 대한 액세스 권한이 있는 공격자는 연결을 통해 악용할 수 있다.
<security-constraint>
<web-resource-collection>
<web-resource-name>RestrictedMethods</web-resource-name>
<url-pattern>/*</url-pattern>
<http-method>DELETE</http-method>
<http-method>SEARCH</http-method>
<http-method>COPY</http-method>
<http-method>MOVE</http-method>
<http-method>PROPFIND</http-method>
<http-method>PORPPATCH</http-method>
<http-method>MKCOL</http-method>
<http-method>LOCK</http-method>
<http-method>UNLOCK</http-method>
<http-method>PUT</http-method>
<http-method>OPTIONS</http-method>
<http-method>TRACE</http-method>
</web-resource-collection>
<auth-constraint/>
</security-constraint>
<security-constraint>
<web-resource-collection>
<web-resource-name>SecurePages</web-resource-name>
<url-pattern>/*</url-pattern>
</web-resource-collection>
<user-data-constraint>
<transport-guarantee>CONFIDENTIAL</transport-guarantee>
</user-data-constraint>
</security-constraint>
7. 기타 애플리케이션 개발 시 고려사항
- no-cache 설정
Spring의 경우 Interceptor 등을 이용하기도 하지만 전통적인 Filter를 이용하는 샘플을 소개한다.
다음 소스는 참고하시기 바랍니다.
- 애플리케이션의 web.xml 설정
<filter>
<filter-name>NoCacheFilter</filter-name>
<filter-class>com.example.NoCacheFilter</filter-class>
</filter>
<filter-mapping>
<filter-name>NoCacheFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
- NoCacheFilter.java
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 java.io.IOException;
public class NoCacheFilter implements Filter {
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
// 캐시 헤더 설정
response.setHeader("Cache-Control", "no-cache, no-store, must-revalidate");
response.setHeader("Pragma", "no-cache");
response.setHeader("Expires", "0");
// 다음 필터로 전달
chain.doFilter(request, response);
}
@Override
public void init(FilterConfig filterConfig) throws ServletException {
// 초기화 코드
}
@Override
public void destroy() {
// 종료 코드
}
}
- SameSite 설정
2000년대 이후 브라우저들은 쿠키의 SameSite 설정이 정해지지 않았을 경우 기본값을 None에서 Lax로 동작하도록 변경하였다. 그러나 세션 값등 특정 쿠키에 대해 SameSite 설정이 필요하다면 별도의 필터를 구현해서 설정해 줘야 한다.
SameSite 에는 None, Lax, Strict 3가지의 옵션이 있으며 이중 과거에는 None이 기본이었으나 현재에는 Lax가 기본값으로 되어 있다.
None : 도메인 검증하지 않음(타 사이트에서 접근 가능)
Lax : 기본적으로는 Strict이나 cross site 요청이라도 get, a href, link rel을 통한 안전한 요청인 경우 허용된다.
Strict : 동일 사이트에서만 쿠키 전송등을 허가하여 보안성이 뛰어나지만 편의성이 떨어진다.
(참고: https://goodteacher.tistory.com/496 )
* Strict로 설정할 경우 타 사이트 팝업만 띄워도 세션쿠키가 갱신되는 괴랄함을 경험했었다.
다음 소스는 참고하시기 바랍니다.
- 애플리케이션의 web.xml 설정
<filter>
<filter-name>SameSiteCookieFilter</filter-name>
<filter-class>com.example.SameSiteCookieFilter</filter-class>
</filter>
<filter-mapping>
<filter-name>SameSiteCookieFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
- SameSiteCookieFilter.java
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.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
public class SameSiteCookieFilter implements Filter {
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
if (response instanceof HttpServletResponse) {
HttpServletResponse httpServletResponse = (HttpServletResponse) response;
addSameSiteCookieAttribute(httpServletResponse);
}
chain.doFilter(request, response);
}
private void addSameSiteCookieAttribute(HttpServletResponse response) {
String headerValue = "SameSite=None; Secure; HttpOnly;";
response.setHeader("Set-Cookie", "myCookie=myValue; " + headerValue);
}
@Override
public void init(FilterConfig filterConfig) throws ServletException {
// 초기화 코드
}
@Override
public void destroy() {
// 종료 코드
}
}
- HttpOnly 설정
CSS(Cross Site Script) 공격 중 하나로 Javascript로 쿠키를 탈취하는 것을 막기 위해 HttpOnly 설정을 할 수 있다.
- 애플리케이션의 web.xml 설정
<session-config>
<session-timeout>300</session-timeout>
<cookie-config>
<http-only>true</http-only>
</cookie-config>
<tracking-mode>COOKIE</tracking-mode>
</session-config>
web.xml 에 http-only를 설정할 경우 해당 웹애플리케이션의 모든 쿠키는 httpOnly 가 되어 버린다.그럴 경우 SameSiteCookieFilter.java 소스처럼 특정 쿠키에 httpOnly를 설정하거나 제외할 수 있다.최근 브라우저의 보안업그레이드로 인해서 http-only가 아닌 세션아이디에 대해서는 정상처리가 되지 않는 것을 확인했다.
로컬 테스트에서는 처리가 되었지만 프록시나, DNS 를 통해서 서비스되는 경우 정상 생성되지 않았다.
* "오늘은 그만 보기" 같은 기능이 쿠키를 사용하는데 httpOnly 가 전역으로 설정되면 쿠키를 사용하지 않거나 해당쿠키를 제외하거나 우회하도록 변경해야 한다.
** 쿠키를 사용하여 클라이언트 사이드 자바스크립트 프로그래밍을 하는것은 보안상 문제가 될 수 있으므로 서버사이드 세션과 연동하거나 LocalStorage를 사용하는 것으로 변경하길 권장한다.
*** LocalStorage 사용시에는 중요데이터를 저장하지 않도록 주의해야 한다.
- Referrer Policy 설정
Referrer Policy는 브라우저가 현재 페이지에서 외부 도메인으로 이동할 때 이전 페이지의 정보(리퍼러)를 어떻게 다룰지를 결정한다.
주로 정보 유출을 방지하기 위해 사용되며, 외부 도메인으로의 요청 시에 이전 페이지의 정보를 최소화한다.
정책으로는 no-referrer, origin, strict-origin, 등의 값을 사용하여 설정된다.
- ReferrerPolicyFilter.java
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.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
public class ReferrerPolicyFilter implements Filter {
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
if (response instanceof HttpServletResponse) {
HttpServletResponse httpServletResponse = (HttpServletResponse) response;
setReferrerPolicy(httpServletResponse);
}
chain.doFilter(request, response);
}
private void setReferrerPolicy(HttpServletResponse response) {
HttpServletResponse httpResponse = (HttpServletResponse) response;
httpResponse.setHeader("Referrer-Policy", "orign");
}
@Override
public void init(FilterConfig filterConfig) throws ServletException {
// 초기화 코드
}
@Override
public void destroy() {
// 종료 코드
}
}
SSO(Single Sign On)를 사용할 경우 상황에 따라서 Referrer Policy가 문제가 될 수 있으니 기본설정인 orign을 이용하면 브라우저는 현재 페이지와 동일한 출처(origin)로의 요청에 대해서만 리퍼러 정보를 전송하게 된다.
그러나 이는 구체적인 SSO 구현 및 요구 사항에 따라 다를 수 있다.
- CSP(Content Security Policy) 설정
콘텐츠 보안 정책(Content-Security policy)은 신뢰할 수 있는 웹 페이지 콘텍스트에서 악의적인 콘텐츠 실행으로 인한 크로스 사이트 스크립팅, 클릭 재킹 및 기타 코드 삽입 공격을 방지하기 위해 도입된 컴퓨터 보안 표준이다.
웹에서 사용하는 콘텐츠(이미지, 스크립트 등등)에 대한 규칙 같은 거라 생각하면 된다.
SOP(Same Origin Policy)와 유사하게 차단한다는 점에서 비슷하지만,
CSP의 경우 웹 사이트가 직접 룰을 적용해서 사용하게 된다. (참고 : https://webstone.tistory.com/98 )
* CSP를 적용할 때에는 javascript, css 등 모든 콘텐츠에 영향이 가므로 사실상 개발초기부터 고려해서 개발해야 한다. 해당 JS 라이브러리 가 CSP를 고려해서 구성되지 않거나(오래된 JS 라이브러리 중에는 보안준수가 미흡한 것들이 존재한다.) 하면 CSP를 적용한 순간 수많은 JS 오류 메시지를 보게 되거나 CSS가 깨진 사이트를 마주하게 될 것이다.
* 아래 코드는 그냥 설정하는 방식을 참고하길 바라며 CSP 정책에 대해서는 MDN() 등 더 참조가 필요하다.
* CSP를 평가해 주는 사이트도 있으니 참고하자(https://csp-evaluator.withgoogle.com/)
* Content-Security-Policy-Report-Only 모드가 있는데 이 모드를 적용해도 JQuery 3.7.1(2024.01 현재) 에서 넘어가질 않는다.
** 개인적으로 경험해 본 결과 다른 보안설정 요소는 WAS 설정이나 필터정의 등으로 해결이 되었으나 CSP 만큼은 XSS이나 SQL Injection급으로 개발하면서 콘텐츠 전반에 모든 요소(HTML, image, JavaScript, CSS 등)에 영향을 주는 것으로 개발 시에 고려되지 않았다면 좀.....
*만약 손쉬운 CSP 설정으로 구 사이트 등에서 보안을 만족할 방법이 있다면 알려주시길 바랍니다.(급 공손 ^_^;;;)
다음 소스는 참고하시기 바랍니다.
- 애플리케이션의 web.xml 설정
<filter>
<filter-name>CSPFilter </filter-name>
<filter-class>com.example.CSPFilter</filter-class>
</filter>
<filter-mapping>
<filter-name>CSPFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
- CSPFilter.java
import javax.servlet.*;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
* <b>Description</b>
* <pre>
* CSP(Content-Security-Policy header) Java
* 참고: https://content-security-policy.com/examples/java/
* </pre>
*
*/
public class CSPFilter implements Filter {
public static final String POLICY = "default-src 'self'; style-src 'self' 'unsafe-inline'; script-src 'self' 'unsafe-inline' http: https:; base-uri 'none'; object-src 'none';";
@Override
public void doFilter(ServletRequest request, ServletResponse response,
FilterChain chain) throws IOException, ServletException {
if (response instanceof HttpServletResponse) {
((HttpServletResponse) response).setHeader(
"Content-Security-Policy", CSPFilter.POLICY);
}
chain.doFilter(request, response);
}
@Override
public void init(FilterConfig filterConfig) throws ServletException {
}
@Override
public void destroy() {
}
}