商用環境で設定しておきたいセキュリティ関連 HTTP ヘッダまとめ

f:id:Naotsugu:20180118215616p:plain

TL;DR

  • X-Content-Type-Options
    • MIME スニッフィングの無効化
  • X-Frame-Options(XFO)
    • フレーム表示を制限しクリックジャッキングを予防
  • X-XSS-Protection
    • XSSフィルタの有効/無効
  • Content-Security-Policy (CSP)
    • XSSなどの攻撃を軽減するセキュリティレイヤー
  • Strict-Transport-Security (HSTS)
    • HTTP の代わりに HTTPS による通信を行い中間者攻撃を低減
  • Public-Key-Pins (HPKP)
    • 偽造された証明書による中間者攻撃を低減


X-Content-Type-Options

MIME スニッフィングを無効化して、Content-Type で指定したタイプを強制的に使用させる。

IE は Content-Type だけでなくURLやコンテンツの内容からコンテンツタイプを独自判断する。 これにより Content-Type の指定が無視される結果となり、 例えば Content-Type: text/plain であっても、HTML と解釈して中身のスクリプトを実行してしまうといったことが起こり得た。

この動作を抑止するには以下のレスポンスヘッダを付ける。

X-Content-Type-Options: nosniff

これは IE8以降から有効。 本ヘッダの弊害は少ないので常に付けておくのがベター。


X-Frame-Options(XFO)

対象のページを <frame><iframe> <object> の中に表示することを許可するかどうかを指示する。

クリックジャッキング攻撃を防止する用途で利用できる。

クリックジャッキング攻撃とは対象のページを iframe 内に読み込み、害のなさそうなページの上に透明にして重ねて表示する。 害の無さそうなボタンを押したつもりが、透明にして上に重ねた iframe 内のボタンを押させるといった攻撃。

自身のページを、他ドメインページのフレーム内へ表示することを禁止するには以下のように指定する。

X-Frame-Options: SAMEORIGIN

設定可能なオプションは以下となる。

オプション 説明
DENY フレーム表示を禁止
SAMEORIGIN 同一ドメイン内に限りフレーム表示を許可
ALLOW-FROM uri 指定された生成元に限り、フレーム表示を許可

SAMEORIGIN を基本とし、可能な場合には DENY にするのがベター。


X-XSS-Protection

IE8以降に導入された、XSSによる攻撃を緩和する XSS Filter 機能の有効/無効を設定する。

IEの場合は、XSS攻撃を検出すると「見込まれるクロスサイト・スクリプト攻撃を防ぐために、このページを変更しました」というアラートが出る。 この時対象のスクリプト部分を # で置き変えて無効化する。

通常 XSS Filter 機能はブラウザのデフォルトで有効に設定されているはず(Chrome や Safari にも同様の機能がある)。

XSS Filter 機能を有効化するにはレスポンスヘッダーにて以下のように指定する。

X-XSS-Protection: 1; mode=block

以下の設定が可能。

設定 説明
X-XSS-Protection: 0 XSS Filter 機能を無効化する
X-XSS-Protection: 1 XSS Filter 機能を有効化する
X-XSS-Protection: 1; mode=block XSSを検知した場合にブラウザ上の表示をブロック(空表示)する

ブラウザのデフォルトで有効に設定されているはずだが、設定する場合は X-XSS-Protection: 1; mode=block で良いと思う。


Content-Security-Policy (CSP)

昔は X-Content-Security-Policy であったが、最新ブラウザでは接頭辞のない Content-Security-Policy がサポートされていく。

ページで読み込むことを許可するリソースを制御する XSS対策機能を設定する。

例えば以下のようにすると、自ドメインと指定したドメイン(google-analytics)のみの読み込みを許可する。

Content-Security-Policy: default-src 'self' www.google-analytics.com

スクリプトの実行に以下の制約が設けられる

  • インラインスクリプトの実行禁止(HTML中の<script>・・・</script>)
  • スクリプト読み込み先の制限(<script src="URI"></script> の URI は自ドメインまたはホワイトリストに含まれる)
  • eval() 関数などによる文字列をスクリプトとしての実行禁止

default-src はデフォルト設定を表し、script-src はスクリプト関連の権限を制御するディレクティブ、 style-src で style 要素関連の権限を制御するディレクティブなどいろいろあり、個別に制約定義できる。

'self' とあるのは参照元リストで、以下が利用できる。

設定 説明
'none' 指定なし
'self' 同一オリジン。サブドメインは除外
'unsafe-inline' インライン JavaScript および CSS を許可
'unsafe-eval' eval などの text-to-JavaScript の仕組みを許可

複雑なので詳細はこちら。

developer.mozilla.org

制約違反が有った場合、指定したURLに JSON形式で報告させることができる。

Content-Security-Policy: default-src 'self'; report-uri https://example.com/csp-report

report-uri にレポートが送信されるようになる。制約を適用せず、監視だけを目的とする場合には Content-Security-Policy-Report-Only を使う。

Content-Security-Policy-Report-Only: default-src 'self'; report-uri https://example.com/csp-report

ページ単位の指定の場合には meta タグでも可。

<meta http-equiv="Content-Security-Policy" content="default-src https://cdn.example.net; child-src 'none'">


Upgrade-Insecure-Requests

HTTPS 時にページ中にHTTPのリンクなどがあった場合、ブラウザで Mixed Contents の警告となる。

常時SSLへの移行仮定で、ページ中にHTTPのリンクが残っている場合に、HTTPS と読み替えてくれる機能がある。

クライアントがこの機能に対応している場合には、リクエストヘッダに以下が入っている。

Upgrade-Insecure-Requests: 1

クライアントが対応している場合には以下のレスポンスヘッダにてクライアントに読み替え指示できる。

Content-Security-Policy: upgrade-insecure-requests


Strict-Transport-Security (HSTS)

HTTP Strict Transport Security(HSTS)は、HTTPS 通信を強制するようブラウザに伝達するセキュリティ機能。

ブラウザは初めて HTTPS 接続をしたときに Strict-Transport-Security ヘッダがあると、Strict Transport Security 機能が有効になり、そのサイトへの接続は自動的に HTTPS 接続を試みるようになる(HTTP接続でこのヘッダが付いていたとしてもブラウザは無視する)。

常時SSLの場合でも、初めてのサイトへの接続にはユーザ利便性のため HTTP 接続を許容する場合がある(直後にHTTPSへリダイレクトする)。 この場合に HSTS を設定することで、それ以降の接続で自動的に HTTPS が使われることになる。

たまに HTTP 接続しているのに、なぜか HTTPS 接続になると困っている人がいるが、たいていこのヘッダが原因。

設定には以下のヘッダを指定する。

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

各オプションは以下

オプション 説明
max-age HSTS設定をブラウザが記憶する秒数
includeSubDomains HSTS設定をサブドメインに適用する場合に指定
preload HSTSプリロードリスト登録時に必要

HSTS プリロードリストは HSTS Preload List Submission で管理されており、申請により特定ドメインの接続がはじめから HTTPS 接続となる対象のリスト。

主要ブラウザではここで管理されているリストの内容で、初回の HTTP アクセスも自動的に HTTPS 接続を強制することができる。


Public-Key-Pins (HPKP)

Public Key Pinning Extension for HTTPは、偽造された証明書による中間者攻撃を防ぐための機能。

レスポンスヘッダに本物の証明書の公開鍵のハッシュ値を Base64 にして付けておくことでブラウザが実際のサーバから送信されてくる証明書の公開鍵データのハッシュ値と比較して、不正な証明書の利用を検知する。

以下のように指定する。

Public-Key-Pins: pin-sha256="base64=="; max-age=5184000; includeSubDomains; report-uri="https://www.example.net/hpkp-report"

base64== の箇所に公開鍵ダイジェストの Base64 エンコード値を設定する。

report-uri で不正な証明書の利用を検知した場合に指定したURLに JSON形式で報告させることができる。

Public-Key-Pins-Report-Only にするとレポートのみを行う。

Public-Key-Pins-Report-Only: pin-sha256="base64=="; max-age=5184000; includeSubDomains; report-uri="https://www.example.net/hpkp-report"


設定

フロントに Apache がいれば Apache で設定すれば良い。

サーブレットの場合は、tomcat で事前定義されたサーブレットフィルター HttpHeaderSecurityFilter が予め用意してあるのでこれを使うとよい。

  • Strict-Transport-Security
  • X-Frame-Options
  • X-Content-Type-Options

以下のような実装になっている。

package org.apache.catalina.filters;

import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;

import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletResponse;

import org.apache.juli.logging.Log;
import org.apache.juli.logging.LogFactory;

/**
 * Provides a single configuration point for security measures that required the
 * addition of one or more HTTP headers to the response.
 */
public class HttpHeaderSecurityFilter extends FilterBase {

    private static final Log log = LogFactory.getLog(HttpHeaderSecurityFilter.class);

    // HSTS
    private static final String HSTS_HEADER_NAME = "Strict-Transport-Security";
    private boolean hstsEnabled = true;
    private int hstsMaxAgeSeconds = 0;
    private boolean hstsIncludeSubDomains = false;
    private String hstsHeaderValue;

    // Click-jacking protection
    private static final String ANTI_CLICK_JACKING_HEADER_NAME = "X-Frame-Options";
    private boolean antiClickJackingEnabled = true;
    private XFrameOption antiClickJackingOption = XFrameOption.DENY;
    private URI antiClickJackingUri;
    private String antiClickJackingHeaderValue;

    // Block content sniffing
    private static final String BLOCK_CONTENT_TYPE_SNIFFING_HEADER_NAME = "X-Content-Type-Options";
    private static final String BLOCK_CONTENT_TYPE_SNIFFING_HEADER_VALUE = "nosniff";
    private boolean blockContentTypeSniffingEnabled = true;

    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
        super.init(filterConfig);

        // Build HSTS header value
        StringBuilder hstsValue = new StringBuilder("max-age=");
        hstsValue.append(hstsMaxAgeSeconds);
        if (hstsIncludeSubDomains) {
            hstsValue.append(";includeSubDomains");
        }
        hstsHeaderValue = hstsValue.toString();

        // Anti click-jacking
        StringBuilder cjValue = new StringBuilder(antiClickJackingOption.headerValue);
        if (antiClickJackingOption == XFrameOption.ALLOW_FROM) {
            cjValue.append(':');
            cjValue.append(antiClickJackingUri);
        }
        antiClickJackingHeaderValue = cjValue.toString();
    }


    @Override
    public void doFilter(ServletRequest request, ServletResponse response,
            FilterChain chain) throws IOException, ServletException {

        if (response.isCommitted()) {
            throw new ServletException(sm.getString("httpHeaderSecurityFilter.committed"));
        }

        // HSTS
        if (hstsEnabled && request.isSecure() && response instanceof HttpServletResponse) {
            ((HttpServletResponse) response).setHeader(HSTS_HEADER_NAME, hstsHeaderValue);
        }

        // anti click-jacking
        if (antiClickJackingEnabled && response instanceof HttpServletResponse) {
            ((HttpServletResponse) response).setHeader(
                    ANTI_CLICK_JACKING_HEADER_NAME, antiClickJackingHeaderValue);
        }

        // Block content type sniffing
        if (blockContentTypeSniffingEnabled && response instanceof HttpServletResponse) {
            ((HttpServletResponse) response).setHeader(BLOCK_CONTENT_TYPE_SNIFFING_HEADER_NAME,
                    BLOCK_CONTENT_TYPE_SNIFFING_HEADER_VALUE);
        }
        chain.doFilter(request, response);
    }


    @Override
    protected Log getLogger() {
        return log;
    }


    @Override
    protected boolean isConfigProblemFatal() {
        // This filter is security related to configuration issues always
        // trigger a failure.
        return true;
    }


    public boolean isHstsEnabled() {
        return hstsEnabled;
    }


    public void setHstsEnabled(boolean hstsEnabled) {
        this.hstsEnabled = hstsEnabled;
    }


    public int getHstsMaxAgeSeconds() {
        return hstsMaxAgeSeconds;
    }


    public void setHstsMaxAgeSeconds(int hstsMaxAgeSeconds) {
        if (hstsMaxAgeSeconds < 0) {
            this.hstsMaxAgeSeconds = 0;
        } else {
            this.hstsMaxAgeSeconds = hstsMaxAgeSeconds;
        }
    }


    public boolean isHstsIncludeSubDomains() {
        return hstsIncludeSubDomains;
    }


    public void setHstsIncludeSubDomains(boolean hstsIncludeSubDomains) {
        this.hstsIncludeSubDomains = hstsIncludeSubDomains;
    }



    public boolean isAntiClickJackingEnabled() {
        return antiClickJackingEnabled;
    }



    public void setAntiClickJackingEnabled(boolean antiClickJackingEnabled) {
        this.antiClickJackingEnabled = antiClickJackingEnabled;
    }



    public String getAntiClickJackingOption() {
        return antiClickJackingOption.toString();
    }


    public void setAntiClickJackingOption(String antiClickJackingOption) {
        for (XFrameOption option : XFrameOption.values()) {
            if (option.getHeaderValue().equalsIgnoreCase(antiClickJackingOption)) {
                this.antiClickJackingOption = option;
                return;
            }
        }
        throw new IllegalArgumentException(
                sm.getString("httpHeaderSecurityFilter.clickjack.invalid", antiClickJackingOption));
    }



    public String getAntiClickJackingUri() {
        return antiClickJackingUri.toString();
    }


    public boolean isBlockContentTypeSniffingEnabled() {
        return blockContentTypeSniffingEnabled;
    }


    public void setBlockContentTypeSniffingEnabled(
            boolean blockContentTypeSniffingEnabled) {
        this.blockContentTypeSniffingEnabled = blockContentTypeSniffingEnabled;
    }


    public void setAntiClickJackingUri(String antiClickJackingUri) {
        URI uri;
        try {
            uri = new URI(antiClickJackingUri);
        } catch (URISyntaxException e) {
            throw new IllegalArgumentException(e);
        }
        this.antiClickJackingUri = uri;
    }


    private static enum XFrameOption {
        DENY("DENY"),
        SAME_ORIGIN("SAMEORIGIN"),
        ALLOW_FROM("ALLOW-FROM");


        private final String headerValue;

        private XFrameOption(String headerValue) {
            this.headerValue = headerValue;
        }

        public String getHeaderValue() {
            return headerValue;
        }
    }
}

web.xml で以下のように読み込む。

<filter>
    <filter-name>HttpHeaderSecurityFilter</filter-name>
    <filter-class>org.apache.catalina.filters.HttpHeaderSecurityFilter</filter-class>
  </filter>
  <filter-mapping>
    <filter-name>HttpHeaderSecurityFilter</filter-name>
    <url-pattern>/*</url-pattern>
  </filter-mapping>


Spring Security の場合は以下がサポートされている。

  • X-Content-Type-Options
  • X-Frame-Options(XFO)
  • X-XSS-Protection
  • Strict-Transport-Security (HSTS)
@EnableWebSecurity
@Configuration
public class WebSecurityConfig extends
   WebSecurityConfigurerAdapter {

  @Override
  protected void configure(HttpSecurity http) throws Exception {
    http
      .headers()
        .cacheControl()
        .contentTypeOptions()
        .hsts()
        .frameOptions()
        .xssProtection()
        .and()
      ...;
  }
}