【Rails】ブラウザのコンソールからのfetchによって、CORS設定の挙動を確認する

Rails: 7.0.2.3

Ruby: 3.1.0

Railsのgem ‘rack-cors’ の動作がよくわからないので、動作確認してみた。HTTPの知識が浅いので動作確認の方法がなかなかわからなかったので、きちんと手順どおりに記録することにした。

確認したいのは、’rack-cors’の gem がなくてもクロスオリジンのリクエストを受け付けないこと。現在作成しているアプリでは Vue.js からリクエストすることはあるが、同一オリジン内でのやり取りに限定されるので、わざわざクロスオリジンを許可する必要がないから。

前提:

  • RailsプロジェクトでAPIのルーティングとコントローラーを用意している。
# config/routes.rb
Rails.application.routes.draw do
  namespace :api do
    resources :rooms, only: '' do
      resources :maps, only: %i[show create update delete]
    end
  end
end
# app/controllers/api/maps_controller.rb

module Api
  class MapsController < ApplicationController
    def show
      @map = Map.find(params[:id])
      render :show
    end

    private

    def map_params
      params.require(:map).permit(:trimming, :expansion, :rotation, :image)
    end
  end
end
# app/views/api/maps/show.json.jbuilder

json.merge! @map.attributes
json.image_url url_for(@map.image)
# app/views/maps/edit.html.slim

div
  h1 マップ画像を編集します

  #maps-edit(room_id="#{@map.room.id}")

  = link_to 'ルームに戻る', room_path(@map.room)
// app/javascript/maps/edit.js

import { createApp } from 'vue'
import MapsEdit from './edit.vue'

document.addEventListener('turbo:load', () => {
  const selector = '#maps-edit'
  const maps_edit = document.querySelector(selector)

  if (maps_edit) {
    const roomId = maps_edit.getAttribute('room_id')
    const app = createApp(MapsEdit)
    app.provide('roomId', roomId)
    app.mount(selector)
  }
})
// app/javascript/maps/edit.vue

<template>
  <section id="maps-edit">
    <div v-if="isImageEdit" @touchmove.prevent>
      <image-edit></image-edit>
      <image-update></image-update>
    </div>
    <div v-else>
      <image-show></image-show>
    </div>
  </section>
</template>

...省略...
  • Vue.js からRailsAPIへリクエストがあるが、上記の仕組みから、同一オリジンとなる。
// app/javascript/components/image_show.vue

<template>
...省略...
</template>

<script>
import api from '../modules/api'

export default {
  name: 'ImageShow',
  ...省略...
  async created() {
    const response = await api.actions.show(`/api/rooms/${this.roomId}/${this.targetModel}s/${this.mapId}.json`)
  }
}
</script>
// app/javascript/modules/api.js

import axios from 'axios'

const actions = {
  async show(url) {
    try {
      const response = await axios.get(url)
      return response.data
    } catch (e) {
      console.error(e)
    }
  },
    ...省略...
}

export default {
  namespaced: true,
  actions
}

参考: https://github.com/kasai441/ruumarker

‘rack-cors’がある場合とない場合、’rack-cors’での設定を変えた場合の3パターンで検証した。以下がそれぞれの期待の動作。

  1. rack-cors gem を入れないデフォルトの状態では、同一オリジン以外からのリクエストがブロックされる。
  2. rack-cors にて、リクエストを許可する origins を限定した時、指定以外のオリジンからのリクエストがブロックされる。また、レスポンスのヘッダーに Access-Control-Allow-Methods と Access-Control-Allow-Origin がそれぞれ設定通り表示される。
  3. rack-cors にて、 origins を全て許可する’*’で指定した時、クロスオリジンでリクエストができる。また、レスポンスのヘッダーに Access-Control-Allow-Methods と Access-Control-Allow-Origin がそれぞれ設定通り表示される。

これらが確認できれば、rack-cors gemを採用しなくともセキュアである根拠となる。

fetchの方法

HTTPの知識が浅いので、自在にリクエストを飛ばすことができずに苦労した。とりあえずヘッダーの必要のないGETで動作確認をしたくて色々調べていて以下のやり方に辿り着いた。

いろいろなオリジンからリクエストをする方法でわかりやすかったのは、異なるWEBアプリ(RailsでもVue CLIでもNode.jsでも)を別ポートで立ち上げ、ブラウザのdev-toolからJavaScriptでfetchによってリクエストするというもの。そうするとNetworkタブでResponseのヘッダーも確認できる。

  • Railsのプロジェクトを複数立ち上げ
# ProjectA
rails server -p 3000
# ProjectB
rails server -p 3001
  • ブラウザのdev-toolでfetch

例えば http://localhost:3000 から http://localhost:3001APIへリクエストしたい場合は、ブラウザで http://localhost:3000 にアクセスして、dev-toolのコンソールを開き、以下のようなJavaScriptを実行する

fetch('http://localhost:3001/routes', {
    method: "GET", 
})
.then(res => res.json())
.then(data => console.log(data))

参考: RailsでAPIにCORSを設定する - Qiita

これを上記のパターンごとに実行し、動作結果をそれぞれ確認した。

1. rack-cors gem を入れない状態では、同一オリジン以外からのリクエストができない

  • Gemfile から’rack-cors’を外し、bundle install する。
  • config/initializers/cors.rb がある場合、削除する。
  • rails server を起動する。(port: 3001)

1-a. クロスオリジンのリクエスト(ポート3000からポート3001)

localhost:3000 に別アプリのサーバーを立ち上げて、ブラウザでアクセスしたのち、dev-toolのコンソールを開いて下記のJavaScriptを実行する。 この場合、api/rooms/{:room_id}/maps/{map_id}.jsonというAPIを叩いている。

1

リクエストがCORSでブロックされたので期待通り。

2

また、CORSの設定がないので当然、レスポンスヘッダーには Access-Control-Allow-Methods と Access-Control-Allow-Origin がない。

1-b. 同一オリジンからのリクエスト(ポート3001 からポート3001)

ブラウザでlocalhost:3001にアクセスして、コンソールから先ほどと全く同じ JavaScript を実行する。

これは、リクエストが通り、レスポンスが帰ってくる。CORSの設定がないのでレスポンスヘッダーも期待通り。

2. rack-cors にて origin を限定した時、指定以外のオリジンからのリクエストができない

  • Gemfile に’rack-cors’を追加し、bundle install する。
  • config/initializers/cors.rb を以下のように設定する。
Rails.application.config.middleware.insert_before 0, Rack::Cors do
  allow do
    origins 'localhost:3002' # ポート3002からのリクエストのみ許可
    resource '*',
         headers: :any,
         methods: %i[get post put delete]
  end
end
  • rails server を立ち上げる。(port: 3001)

2-a. 指定外オリジンからのリクエスト(ポート3000 からポート3001)

localhost:3000 に別アプリのサーバーを立ち上げて、ブラウザでアクセスしたのち、dev-toolのコンソールを開いて下記のJavaScriptを実行する。

クロスオリジンでCORSでも設定されていないポート3000からのリクエストはブロックされる。期待通り。

2-b. 同一オリジンのリクエスト(ポート3001 からポート3001)

localhost:3001 にブラウザでアクセスしたのち、dev-toolのコンソールを開いて同様のJavaScriptを実行する。

レスポンスヘッダーには Access-Control-Allow-Methods と Access-Control-Allow-Origin がないが、同一オリジンなので、CORSに設定されていないがリクエストは通る。期待通り。

2-c. 指定オリジンからのリクエスト(ポート3002 からポート3001)

localhost:3002 に別アプリのサーバーを立ち上げて、ブラウザでアクセスしたのち、dev-toolのコンソールを開いて同様のJavaScriptを実行する。

クロスオリジンだが、CORSで指定したオリジンからのリクエストなので、レスポンスヘッダーに Access-Control-Allow-Methods と Access-Control-Allow-Origin が記載されており、リクエストが通る。これが’rack-cors’の期待通りの動作。

3. rack-cors にて origin を’*’で指定した時、クロスオリジンでリクエストができる

  • Gemfile に’rack-cors’を追加し、bundle install する。
  • config/initializers/cors.rb を以下のように設定する。
    Rails.application.config.middleware.insert_before 0, Rack::Cors do
      allow do
        origins '*' # 全てのオリジンを許可
        resource '*',
                 headers: :any,
                 methods: %i[get post put delete]
      end
    end
  • rails server を立ち上げる。(port: 3001)

3-a. クロスオリジンのリクエスト(ポート3000 からポート3001)

localhost:3000 に別アプリのサーバーを立ち上げて、ブラウザでアクセスしたのち、dev-toolのコンソールを開いて下記のJavaScriptを実行する。

CORSに全てのオリジンを指定しているのでレスポンスヘッダーに Access-Control-Allow-Methods と Access-Control-Allow-Origin が記載されており、リクエストも通る。

3-b. 同一オリジンのリクエスト(ポート3001 からポート3001)

localhost:3001 にブラウザでアクセスしたのち、dev-toolのコンソールを開いて同様のJavaScriptを実行する。

同一オリジンの場合はレスポンスヘッダーは変更されないようだ。いずれにしても、この場合リクエストは通る。

結果

以上で考えられる全てのパターンで動作確認できた。どうやら同一オリジン間でのリクエストには’rack-cors’は働かないようで、レスポンスヘッダーが変化しなかった。クロスオリジンのリクエストの場合、cors.rb で指定したオリジンのみ、レスポンスヘッダーに Access-Control-Allow-Methods と Access-Control-Allow-Origin が記載され、リクエストが許可されることが確認できた。

Railsではデフォルトでクロスオリジンリクエストがブロックされているようで、わざわざ’rack-cors’を入れなくてもセキュアだし、’rack-cors’を導入して全てのオリジンを許可する設定にしたりすると、かえってセキュリティが甘くなることも分かってよかった。

CORSはリクエストを完全にブロックできない

当初は、CORSが外部からのリクエストを完全にコントロールしてくれるものと思っていた。しかし、同一オリジンであればブロックされないので、例えば今回のようにWEBアプリのアドレスに直接アクセスした状態でJavaScriptを実行すればリクエストは可能。

または、curlで以下のようにリクエストしてもGETできるようだ。

curl -v http://127.0.0.1:3001/api/rooms/TIciF1vZjzUfq4dqpHKQ/maps/106.json
*   Trying 127.0.0.1:3001...
* Connected to 127.0.0.1 (127.0.0.1) port 3001 (#0)
> GET /api/rooms/TIciF1vZjzUfq4dqpHKQ/maps/106.json HTTP/1.1
> Host: 127.0.0.1:3001
> User-Agent: curl/7.79.1
> Accept: */*
> 
* Mark bundle as not supporting multiuse
< HTTP/1.1 200 OK
< X-Frame-Options: SAMEORIGIN
< X-XSS-Protection: 0
< X-Content-Type-Options: nosniff
< X-Download-Options: noopen
< X-Permitted-Cross-Domain-Policies: none
< Referrer-Policy: strict-origin-when-cross-origin
< Content-Type: application/json; charset=utf-8

...中略...

< Vary: Origin
< Transfer-Encoding: chunked
< 
* Connection #0 to host 127.0.0.1 left intact
{"id":106,"trimming":"{\"x\":116,\"y\":-62}", ...中略...}

なので、APIでやり取りするデータは公開されているものとして扱うべきだと思った。すくなくともCORSはそれを隠蔽するような機能ではなく、あくまで他オリジンからのリクエストをブロックする機能にのみ働くので、たとえばCSRFを防ぎたいという意図なら、CSRF-TOKENをサーバーサイド側で認証するなどの機能をONにしておく必要がある。これまたRailsではデフォルトでそうなっている。

参考にしたサイト

Rails Dockerでリクエストとばして確認

Rails以外の、リクエストとレスポンスの挙動の話 設定の仕方は出ていない

Ajax通信で、と書いてある

⇒ 昔はJavaScriptから他のAPIへリクエストする手段がなかったがCORSがそれを解決した、とある