AngularJS の $http サービスのXSRF対策について調べた

AngularJS の $http サービスのXSRF対策について調べた

AngularJS の $http サービスにはXSRFCSRF)の対策の機能が実装されています。 (AngularJSでは、cross-site request forgeries の略を XSRF と呼んでいる場面が多くみられるので、ここではXSRFで統一します。)

AngularJS側の動作

XHRが実行された際に $http サービスは、CookieXSRF-TOKEN があれば、 X-XSRF-TOKEN という名前でHTTPヘッダーにセットします。※ この名前は変更することができます。

サーバ側の対応

送られてきたCookieに含まれる XSRF-TOKEN の値と HTTPヘッダーのX-XSRF-TOKEN の値が合っていることを確認します。

自分のドメイン上で実行されるJavaScript以外はCookieを読み込むことが出来ないため、 サーバーはXHRが自分のドメイン上で実行されて送られてきた事を保証することが出来ます。

実装して確認してみる

簡単な実装を行い動作を確認してみます。 テキストボックスに入力した値をXHRでサーバにリクエストし、Tokenの検証が成功したら入力した値を返し、テキストボックスの直下に値を表示させます。 構成としては、sinatraで画面(html+AngularJS)をレンダリングし、そのレンダリングした画面でXHRが実行するようにします。

XHRでメッセージをリクエストし、レスポンスで同じメッセージが返ってくれば成功です。

require 'sinatra'
require 'sinatra/reloader'
require 'sinatra/cookies'
require 'json'
require 'active_support'
require 'active_support/core_ext'

# 画面をレンダリングする
get '/' do
  # Tokenを生成
  token = SecureRandom.base64(32)

  response.set_cookie(
    'XSRF-TOKEN',
    value: token,
    # jsからcookieを読み取る必要があるので httponly を off にする
    httponly: false,
    secure: false
  )
  erb :index
end

# Tokenの検証に成功したら、入力した文字をそのまま返す
post '/add_message' do
  if same_xsrf_token?(cookies['XSRF-TOKEN'])
    { message: request_params['message'] }.to_json
  else
    status 403
    { error: 'CSRF検証に失敗しました' }.to_json
  end
end

private

def request_params
  request_body = request.body.read
  return {} if request_body.blank?
  JSON.parse(request_body)
end

def same_xsrf_token?(token)
  return false if token.blank?

  http_headers = request.env.select { |k, v| k.start_with?('HTTP_') }

  token == http_headers['HTTP_X_XSRF_TOKEN']
end

フロント側

<!DOCTYPE html>
<html ng-app='App'>

<head>
  <meta charset="utf-8">
  <title>angularjs xsrf</title>
  <script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.5.6/angular.min.js"></script>
  <script src="js/controller.js"></script>
</head>

<body ng-controller="AppController">
  <input type="text" ng-model="message">
  <input type="button" value="送信" ng-click="send_message()">
  <h1>{{receive_message}}</h1>
</body>
var app = angular.module('App', []);

app.controller('AppController', ['$scope', '$http', function($scope, $http){
  $scope.send_message = function(){
    $http({
      method: 'POST',
      url: 'add_message',
      data: { message: $scope.message }
    })
    .success(function(data, status, headers, config){
      $scope.receive_message = data.message;
    })
    .error(function(data, status, headers, config){
      $scope.receive_message = data.error;
    });
  }
}]);

動作させてみる

画面で確認

  • 無事リクエストしたメッセージが取得できていることを確認しました。

f:id:takapi86:20171223225437p:plain

  • ちなみに検証に失敗するとこうなります。

f:id:takapi86:20171223225500p:plain

リクエストの内容を確認する

POST /add_message HTTP/1.1
Host: localhost:4567
Connection: keep-alive
Content-Length: 20
Accept: application/json, text/plain, */*
Origin: http://localhost:4567
X-XSRF-TOKEN: VGqbpCECjRRXF6XJFwD4IJA0zxzxdobSr4mnMJieyvI=
User-Agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/62.0.3202.75 Safari/537.36
Content-Type: application/json;charset=UTF-8
Referer: http://localhost:4567/
Accept-Encoding: gzip, deflate, br
Accept-Language: ja,en-US;q=0.9,en;q=0.8
Cookie: XSRF-TOKEN=VGqbpCECjRRXF6XJFwD4IJA0zxzxdobSr4mnMJieyvI=

Cookie XSRF-TOKEN に保存されているのと同じ値が、 X-XSRF-TOKEN が付与されていることを確認