LoginSignup
3
4
お題は不問!Qiita Engineer Festa 2024で記事投稿!
Qiita Engineer Festa20242024年7月17日まで開催中!

レスポンスヘッダーのセキュリティに必要な設定全て調べてみた

Posted at

はじめに

会社のとある勉強会で、MDN Web Docs を読む機会があり、レスポンスヘッダには多くの種類があることを学びました(cf. https://developer.mozilla.org/ja/docs/Web/HTTP/Headers )。これまで、CORSぐらいしか知らなかったのですが、セキュリティ的に設定が推奨されるものもありそうだったので、まとめてみました

なお、コードは以下のリポジトリで公開しています。

結論

フロントエンドの設定は

nginx.conf
http {
    server {
        # フロントエンドの設定
        location / {
            ...
            
            # connect-src: フロントエンドでバックエンドのAPIサーバーを利用するために許可する
            # style-src: Vue.jsを利用する場合はselfだけでは効かないので、unsafe-inlineも許可する(ここはより適切な設定ができるかも)
            add_header Content-Security-Policy "
                default-src 'self';
                connect-src 'self' http://localhost:8080;
                style-src 'self' 'unsafe-inline';
                object-src 'none';
                base-uri 'self';
                form-action 'self';
                frame-ancestors 'none';
            ";
            add_header Cross-Origin-Opener-Policy "same-origin";
            add_header Cross-Origin-Resource-Policy "same-origin";
            add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
            add_header X-Content-Type-Options "nosniff";
            add_header X-Frame-Options "DENY";
            add_header X-Permitted-Cross-Domain-Policies "none";
            add_header Permissions-Policy "geolocation=(), microphone=(), camera=()";
            add_header Cross-Origin-Embedder-Policy "require-corp";
            proxy_set_header X-Powered-By "";

            ...
        }
    }
}

バックエンド(FastAPI)の設定は

main.py
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import JSONResponse
from pydantic import BaseModel

app = FastAPI()

headers = {
    'Content-Security-Policy': "default-src 'none'; frame-ancestors 'none';",
    'Cross-Origin-Resource-Policy': 'same-origin',
    'Strict-Transport-Security': 'max-age=31536000; includeSubDomains',
    'X-Powered-By': "",
}

@app.get("/todos")
def read_todos() -> JSONResponse:
    ...
    return JSONResponse(content=content, headers=headers)

app.add_middleware(
    CORSMiddleware,
    allow_origins=["http://localhost"],
    allow_credentials=True,
    allow_methods=["*"],
    allow_headers=["*"],
)


if __name__ == "__main__":
    import uvicorn

    uvicorn.run("main:app", host="0.0.0.0", port=8080, reload=True)

デフォルトのレスポンスヘッダー

まず最初に、何も設定していない(バックエンドのCORSのみ設定しないと通信できないので、設定しています)状態で、リクエストを送ってみて、レスポンスヘッダを確認します

この状態のコードをタグで置いています
https://github.com/eycjur/api_sample/tree/minimum

アプリは単純なTODOアプリで、フロントエンドはTypeScript(Vue.js)、バックエンドはPython(FastAPI)で作成しています。
動作としては、フロントエンドのURLにアクセスすると、フロントエンドのコードを取得します。初期データとして適当なTODOをバックエンドに持たせており、フロントエンドからそのデータを取得も行います。さらに、画面からTODOを追加することもでき、バックエンドにPOSTリクエストを送信し、成功したらTODOが追加されます。

image.png

フロントエンド(Vue.js)
Todos.vue
<script setup lang="ts">
import { ref } from 'vue'

const backendApiUrl = "http://localhost:8080"

const newTodo = ref("")
const todos = ref(Array<string>())

const getTodos = async () => {
  const response = await fetch(`${backendApiUrl}/todos`)
  todos.value =  await response.json()
}
getTodos()

const addTodo = async () => {
  if (newTodo.value.trim() === "") return
  const response = await fetch(`${backendApiUrl}/todos`, {
    method: "POST",
    headers: {
      "Content-Type": "application/json"
    },
    body: JSON.stringify({
      todo: newTodo.value
    }),
  })
  if (!response.ok) {
    console.error("Failed to add todo")
    return
  }
  todos.value.push(newTodo.value)
  newTodo.value = ""
}

</script>

<template>
  <h1>ToDos</h1>
  <div v-for="todo in todos">
    <span>{{ todo }}</span>
  </div>
  <div>
    <input type="text" v-model="newTodo" />
    <button @click="addTodo">Add</button>
  </div>
</template>
nginx.conf
nginx.conf
worker_processes auto;

events {
    worker_connections 1024;
}

http {
    server {
        listen 80;

        server_name localhost;

        # フロントエンドの設定
        location / {
            proxy_pass http://localhost:5173;
            proxy_set_header Host $host;
            proxy_set_header X-Real-IP $remote_addr;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
            proxy_set_header X-Forwarded-Proto $scheme;
        }
    }
}
バックエンド(FastAPI)
main.py
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from pydantic import BaseModel

app = FastAPI()

todos = [
	"sample todo 1",
    "sample todo 2",
    "sample todo 3",
]

class TodoBody(BaseModel):
    todo: str

@app.get("/todos")
def read_todos() -> list[str]:
    return todos

@app.post("/todos")
def create_todos(body: TodoBody) -> dict[str, str]:
    todos.append(body.todo)
    return {"message": "Todo created successfully!"}


app.add_middleware(
    CORSMiddleware,
    allow_origins=["http://localhost"],
    allow_credentials=True,
    allow_methods=["*"],
    allow_headers=["*"],
)


if __name__ == "__main__":
    import uvicorn

    uvicorn.run("main:app", host="0.0.0.0", port=8080, reload=True)

まず、最初にフロントエンドのコードを取得する通信のレスポンスヘッダーは最低限のもののみになっていそうです。
image.png

同様にバックエンドからの通信もCORSで設定したもののみが設定されています
image.png

セキュリティ上設定した方が良いレスポンスヘッダー一覧

いくつかの記事を調べて、出てきたレッダーの一覧を設定するかどうかの候補とし、各候補について役割と、どのように設定するのかをまとめました。

ライブラリを使えばいいという話もありますが、全てのフレームワークに対応するものがあるわけではない&勉強も兼ねて調べています

まず、レスポンスヘッダーの候補は、 OWASP Secure Headers Projectのベストプラクティス に記載されているものや、
レスポンスヘッダを追加してセキュリティを高めてくれるライブラリ(Helmet, Secweb)に実装されているもの、Vue.jsのセキュリティのベストプラクティスに関するブログ、ChatGPTに聞いた結果から洗い出しました。

ヘッダ名 OWASP Helmet Secweb Vue.jsに関するブログ ChatGPTに聞いた結果
1.Content-Security-Policy
2.Cross-Origin-Opener-Policy × ×
3.Cross-Origin-Resource-Policy ×
4.Origin-Agent-Cluster × × ×
5.Referrer-Policy
6.Strict-Transport-Security
7.X-Content-Type-Options
8.X-DNS-Prefetch-Control × × ×
9.X-Download-Options × × ×
10.X-Frame-Options
11.X-Permitted-Cross-Domain-Policies × ×
12.X-Powered-By × × × ×
13.X-XSS-Protection × ×
14.Permissions-Policy × × ×
15.Expect-CT × × × ×
16.Clear-Site-Data × ×
17.HTTP Strict Transport Security(HSTS) for WebSockets × × × ×
18.Cross-Origin-Embedder-Policy × × ×
19.Cache-Control × × ×

以下、それぞれの候補について、役割とどのように設定すべきかを考えていきます

1. Content-Security-Policy (CSP)

役割: 指定されたソースからのみコンテンツを読み込むようにブラウザに指示することで、クロスサイトスクリプティング攻撃(XSS)防止します。

フロントエンド:デフォルトでは、自サイト(self)からのリソース読み込みのみを許可し、connect-srcで自サイトおよびバックエンドのAPIサーバーを許可する。style-srcはVue.jsを利用する場合はselfだけでは効かないので、unsafe-inlineも許可する(ここはより適切な設定ができるかもしれないです)

nginx.conf
http {
    server {
        listen 80;
        server_name localhost;
        # フロントエンドの設定
        location / {
            add_header Content-Security-Policy "
                default-src 'self';
                connect-src 'self' http://localhost:8080;
                style-src 'self' 'unsafe-inline';
                object-src 'none';
                base-uri 'self';
                form-action 'self';
                frame-ancestors 'none';
            ";
        ...

バックエンド:APIのみの利用であれば基本的に指定しても効果はないとのことですが、実装しても害ではないとのことです。( https://stackoverflow.com/questions/45630376/shall-i-use-the-content-security-policy-http-header-for-a-backend-api )

main.py
headers = {
    'Content-Security-Policy': "default-src 'none'; frame-ancestors 'none';",
}

# 全てのエンドポイントのレスポンスに対して、指定したヘッダーを付与してください
# 以下は一例です
@app.get("/todos")
def read_todos() -> JSONResponse:
    return JSONResponse(content=todos, headers=headers)

なお、本記事で利用しているCSPは導入コストの観点から、Level2を利用していますが、多くのブラウザではLevel3に対応しているので、Level3を利用することが望ましいようです。

2. Cross-Origin-Opener-Policy

役割: ページが他のページによって開かれた際に、元のページにアクセスすることを制限し、セキュリティを向上させます。

フロントエンド:他のサイトを開くことを想定しない場合はsame-originを指定します。ただし、OAuthや支払いなどの処理を行う場合は、same-origin-allow-popupsのような設定にする必要があります。

nginx.conf
http {
    server {
        listen 80;
        server_name localhost;
        # フロントエンドの設定
        location / {
            add_header Cross-Origin-Opener-Policy "same-origin";
        ...

バックエンド:APIのみの利用であれば指定は不要です

3. Cross-Origin-Resource-Policy

役割: 他のオリジンからリソースへのアクセスを制限する。

あくまで、Webブラウザにおける保護機能であり、レスポンスボディを書き換えているわけではないので、Webブラウザ以外からはレスポンスボディを確認できてしまう点に注意

フロントエンド:他のリソースからのアクセスを想定しない場合はsame-originで設定しましょう

nginx.conf
http {
    server {
        listen 80;
        server_name localhost;
        # フロントエンドの設定
        location / {
            add_header Cross-Origin-Resource-Policy "same-origin";
        ...

バックエンド:APIのみの利用であれば指定は不要です

4. Origin-Agent-Cluster

役割: オリジンベースのプロセス分離を有効にします。
実装有無: この機能は実験的機能であり、FirefoxやSafariでは利用できません。ブラウザは要求を尊重する必要がないという記述もあることから、設定は必須ではありません。

5. Referrer-Policy

役割: リファラーヘッダで送信される際の情報量を制御します。あるサイト上で機密情報を含むGETリクエストを送信した(クエリパラメータに機密情報が含まれている状態)の後に、そのサイト内にある外部リンクを踏むとリファラーヘッダとして機密情報が漏洩してしまいます
実装有無: 主要ブラウザの設定はstrict-origin-when-cross-originがデフォルトです(参考: https://web.dev/articles/referrer-best-practices?hl=ja )。なお、セキュリティ的にはno-referrerが推奨されていることが多いですが、副作用もある(cf. https://blog.jxck.io/entries/2024-05-07/referrer-policy.html )ので各自で判断してください

6. Strict-Transport-Security (HSTS)

役割: HTTPで接続した場合、ブラウザに対してHTTPS接続を強制するように指示します。

フロントエンド、バックエンドともに実装した方が良いでしょう。なお、31536000は1年間という意味です。

nginx.conf
http {
    server {
        listen 80;
        server_name localhost;
        # フロントエンドの設定
        location / {
            add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
        ...
main.py
headers = {
    'Strict-Transport-Security': 'max-age=31536000; includeSubDomains',
}

7. X-Content-Type-Options

役割: ブラウザがサーバーから指定されたMIMEタイプに従うようにします。

フロントエンド:設定しておく方が良いと思われます。

nginx.conf
http {
    server {
        listen 80;
        server_name localhost;
        # フロントエンドの設定
        location / {
            add_header X-Content-Type-Options "nosniff";
        ...

バックエンド:cssやスクリプトを配信しないので不要です

8. X-DNS-Prefetch-Control

役割: ブラウザのDNSプリフェッチ(ブラウザがリンク先のドメインのDNSレコードを事前に解決することで、リンクをクリックした際の応答速度を向上させる技術)を制御します。
実装有無: サイトの高速化の要件に応じて設定してください

9. X-Download-Options

役割: ファイルのダウンロード時の挙動を制御するために使用されます。特に、ダウンロードされたファイルが自動的に実行されるのを防ぎます。(主にInternet Explorerの脆弱性対策)
実装有無:IEが非推奨になった現在では指定は不要だと思われます。

10. X-Frame-Options

役割: ページがフレームに表示されることを防ぎ、クリックジャッキング攻撃を防止します。

フロントエンド:設定した方が良いでしょう

nginx.conf
http {
    server {
        listen 80;
        server_name localhost;
        # フロントエンドの設定
        location / {
            add_header X-Frame-Options "DENY";
        ...

バックエンド:APIのみの利用であれば指定は不要です

11. X-Permitted-Cross-Domain-Policies

役割: Adobe製品(Flash、PDF、Adobe Acrobatなど)でのクロスドメインポリシーを制御します。

フロントエンド:Adobe製品を利用しない場合は禁止して問題ありません

nginx.conf
http {
    server {
        listen 80;
        server_name localhost;
        # フロントエンドの設定
        location / {
            add_header X-Permitted-Cross-Domain-Policies "none";
        ...

12. X-Powered-By

役割: Webサーバーやフレームワークの情報が露出していると、脆弱性が発覚した場合に、攻撃されてしまうため、隠すことが望ましいです。

フロントエンド、バックエンド:フレームワーク側でデフォルトで付与するかどうかが決まっています。ここでは、付与するフレームワークを使う場合を考慮して、設定例を記載します。(Vue.jsもFastAPIも付与しないので、本来は設定不要です。)

nginx.conf
http {
    server {
        listen 80;
        server_name localhost;
        # フロントエンドの設定
        location / {
            proxy_set_header X-Powered-By "";
        ...
main.py
headers = {
    'X-Powered-By': "",
}

13. X-XSS-Protection

役割: クロスサイトスクリプティング(XSS)フィルタを有効にしますが、非推奨の機能です。
実装有無: 非推奨の機能で、Content-Security-Policyでカバーされるので、実装する必要はありません。

14. Permissions-Policy

役割: ウェブサイトやiframeの要素がブラウザの機能(例:カメラ、マイク、ジオロケーション)を利用できるかどうかを制御します。

フロントエンド: セキュリティとプライバシーの観点から、利用しない機能は明示的に制御することがよさそうです。

nginx.conf
http {
    server {
        listen 80;
        server_name localhost;
        # フロントエンドの設定
        location / {
            add_header Permissions-Policy "geolocation=(), microphone=(), camera=()";
        ...

バックエンド:APIのみの利用であれば指定は不要です

15. Expect-CT

役割: 証明書透明性の要件を定義し、不正な証明書の検出を支援します。
実装有無: 非推奨の機能なので、実装不要です。(日本語版のMDN Web Docsでは特に記載はありませんが、英語版では非推奨とされています: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Expect-CT

16. Clear-Site-Data

役割: サイトのデータ(キャッシュ、クッキー、ストレージなど)をクリアするようブラウザに指示します。
実装有無: ユーザーのログアウト時など、必要に応じて実装してください

17. HTTP Strict Transport Security(HSTS) for WebSockets

役割: WebSocket接続に対してHTTPS接続を強制します。
設定場所: WebSocketsを利用する場合は、Strict-Transport-Securityと同様に設定してください。

18. Cross-Origin-Embedder-Policy

役割: 外部オリジンからのリソースを、読み込むかどうかを制限します。

フロントエンド:CORPまたはCORSの設定だけでは不十分らしい( https://web.dev/articles/why-coop-coep?hl=ja )ので、設定する方がよさそうです。

nginx.conf
http {
    server {
        listen 80;
        server_name localhost;
        # フロントエンドの設定
        location / {
            add_header Cross-Origin-Embedder-Policy "require-corp";
        ...

バックエンド:APIのみの利用であれば指定は不要です

19. Cache-Control

役割: ブラウザのキャッシュ動作を制御します。
実装有無: キャッシュの利用方法に応じて設定してください。

最終的なレスポンスヘッダー

以上の設定を行うと、次のようなレスポンスヘッダーになりました。

フロントエンド
image.png

バックエンド
image.png

また、実装は次のようになります

フロントエンド(Vue.js)
Todos.vue
<script setup lang="ts">
import { ref } from 'vue'

const backendApiUrl = "http://localhost:8080"

const newTodo = ref("")
const todos = ref(Array<string>())

const getTodos = async () => {
  const response = await fetch(`${backendApiUrl}/todos`)
  todos.value =  await response.json()
}
getTodos()

const addTodo = async () => {
  if (newTodo.value.trim() === "") return
  const response = await fetch(`${backendApiUrl}/todos`, {
    method: "POST",
    headers: {
      "Content-Type": "application/json"
    },
    body: JSON.stringify({
      todo: newTodo.value
    }),
  })
  if (!response.ok) {
    console.error("Failed to add todo")
    return
  }
  todos.value.push(newTodo.value)
  newTodo.value = ""
}

</script>

<template>
  <h1>ToDos</h1>
  <div v-for="todo in todos">
    <span>{{ todo }}</span>
  </div>
  <div>
    <input type="text" v-model="newTodo" />
    <button @click="addTodo">Add</button>
  </div>
</template>
nginx.conf
nginx.conf
worker_processes auto;

events {
    worker_connections 1024;
}

http {
    server {
        listen 80;

        server_name localhost;

        # フロントエンドの設定
        location / {
            # connect-src: フロントエンドでバックエンドのAPIサーバーを利用するために許可する
            # style-src: Vue.jsを利用する場合はselfだけでは効かないので、unsafe-inlineも許可する(ここはより適切な設定ができるかも)
            add_header Content-Security-Policy "
                default-src 'self';
                connect-src 'self' http://localhost:8080;
                style-src 'self' 'unsafe-inline';
                object-src 'none';
                base-uri 'self';
                form-action 'self';
                frame-ancestors 'none';
            ";
            add_header Cross-Origin-Opener-Policy "same-origin";
            add_header Cross-Origin-Resource-Policy "same-origin";
            add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
            add_header X-Content-Type-Options "nosniff";
            add_header X-Frame-Options "DENY";
            add_header X-Permitted-Cross-Domain-Policies "none";
            add_header Permissions-Policy "geolocation=(), microphone=(), camera=()";
            add_header Cross-Origin-Embedder-Policy "require-corp";
            proxy_set_header X-Powered-By "";

            proxy_pass http://localhost:5173;
            proxy_set_header Host $host;
            proxy_set_header X-Real-IP $remote_addr;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
            proxy_set_header X-Forwarded-Proto $scheme;
        }
    }
}
バックエンド(FastAPI)
main.py
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import JSONResponse
from pydantic import BaseModel

app = FastAPI()

todos = [
	"sample todo 1",
    "sample todo 2",
    "sample todo 3",
]

headers = {
    'Content-Security-Policy': "default-src 'none'; frame-ancestors 'none';",
    'Cross-Origin-Resource-Policy': 'same-origin',
    'Strict-Transport-Security': 'max-age=31536000; includeSubDomains',
    'X-Powered-By': "",
}

class TodoBody(BaseModel):
    todo: str

@app.get("/todos")
def read_todos() -> JSONResponse:
    return JSONResponse(content=todos, headers=headers)

@app.post("/todos")
def create_todos(body: TodoBody) -> JSONResponse:
    todos.append(body.todo)
    return JSONResponse(content={"message": "Todo created successfully!"}, headers=headers)



app.add_middleware(
    CORSMiddleware,
    allow_origins=["http://localhost"],
    allow_credentials=True,
    allow_methods=["GET", "POST"],
    allow_headers=["*"],
)


if __name__ == "__main__":
    import uvicorn

    uvicorn.run("main:app", host="0.0.0.0", port=8080, reload=True)

ここまで読んでいただきありがとうございました。お疲れさまでした。

参考

(本文中で直接示したものを除く)

3
4
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
4