eval族のスコープについてまとめた

ややこしかったので、まとめました。

eval(Bindingなし)

実行中のコンテキストに出現する変数に対しての操作が可能

foo = "foo"
eval('p foo') #=> foo

eval(Bindingあり)

以下のように、Bindingオブジェクトを使うことで、以下のようにコンテキストを指定できます。

class C
  def instance_binding
    foo = "foooo"
    binding
  end
end

foo = "foo"
binding_object =  C.new.instance_binding
eval('p foo', binding_object) #=> foooo

# このようにも書ける

binding_object.eval('p foo') #=> foooo

module_eval, class_eval

module_evalclass_eval の別名です。 文字列を渡した場合とブロックを渡した場合で、どのスコープで評価されるかが変わります。

文字列を引数とした場合は、レシーバーのスコープで評価されます。

class C; end

C.class_eval(<<-EOF)
Foo = "bar"
def hoge
  p Foo
end
EOF

Foo = "foo"

ブロックを引数とした場合は、そのコンテキストのスコープで評価されます。 つまり、以下ではトップレベルで定義したことになります。

class C; end

C.class_eval do
  Foo = "bar"
  def hoge
    p Foo
  end
end

Foo = "foo"

C.new.hoge
# => warning: already initialized constant Foo
# warning: previous definition of Foo was here
# "foo"

p Object.const_get(:Foo) # => "foo"

instance_eval

module_eval, class_eval と同じく、文字列で渡すときとブロックで渡すときで変わります。

module_exec, class_exec

module_execclass_exec の別名です。

ブロックで渡されたときのスコープは、module_eval と同じです。 文字列での評価はできません。

class C; end

C.class_exec do
  Foo = "bar"
  def hoge
    p Foo
  end
end

Foo = "foo"

C.new.hoge

また、引数をとりブロック引数として、評価する式に値を渡すことができます。

class C; end

C.class_exec(:foo) do |foo|
  define_method(foo) do
    p "foo"
  end
end

C.new.foo #=> "foo"

instance_exec

module_execclass_exec と同じく、引数で値を渡すことができます。

参考

Rubyの定数参照について頭を整理した

最近は、空いた時間にRuby認定試験(Gold)の対策をやっているのですが、 本を読んでいてわかった気になっていた箇所がまぁ、多いこと。。。

練習問題をやってみると、そのへんがはっきりをわかるので良いです。

今回はかなり基本的なことなのですが、定数参照について頭を整理するためにまとめていこうと思います。

Rubyの定数を参照する順番

レキシカルスコープの探索 -> クラス探索 の順番で探索します。

例えば、以下の場合は、CONST_A が表示されます。

class A
  NAME = "CONST_A"

  def name
    NAME
  end
end

class B < A
  NAME = "CONST_B"
end

puts B.new.name #=> CONST_A

BクラスにAのメソッド def name ~ end を追加すると、CONST_B が表示されます。 また、NAME = "CONST_B" を消すと、継承チェーンをたどって、CONST_A が表示されるようになります。

ネストしている場合

もちろんですがネストしている場合、階層が異なるものはそれぞれ別の値が保持されているので、そのスコープに合わせて参照されます。

module M
  class A
    CONST = "M::A"

    def say
      CONST
    end
  end
end

module M
  module B
    class A
      CONST = "M::B::A"

      def say
        CONST
      end
    end
  end
end

ma = M::A.new
puts ma.say # => M::A
mba = M::B::A.new
puts mba.say # => M::B::A

では、この場合はどうなるかというと

module M
  CONST = "Hello"
end

module M
  class C
    def say
      CONST
    end
  end
end

puts M::C.new.say #=> Hello

moduleを再オープンしている場合でも、値は保持されているので Hello が表示されます。

以下のように M::C と記述するとクラスMの探索は行われないようです。

module M
  CONST = "Hello"
end

class M::C
  def say
    CONST
  end
end

puts M::C.new.say #=> uninitialized constant M::C::CONST (NameError)

Cクラスにいる定数は参照可

module M
  class C
    CONST = "Hello"
  end
end

class M::C
  def say
    CONST
  end
end

puts M::C.new.say #=> Hello

includeなどが入ったとき

続いて、includeやprependなどが入ったとき。 こちらも同じく、継承チェーンのどこにクラスが入ってくるかわかれば問題なさそうです。

class B
  CONST = "Hello B"
end

module C
  CONST = "Hello C"
end

module D
  CONST = "Hello D"
end

class A < B
  include C
  include D

  def say
    CONST
  end
end

a = A.new
p a.say # => Hello D

呼び出したところから、一番近い Hello D が参照されます。

A.ancestors
=> [A, D, C, B, Object, Kernel, BasicObject]

まとめ

定数の参照については、

  • 呼び出している箇所はどこか?
  • 定数はどのクラス・モジュールにいるのか?
  • レキシカルスコープはどうなっているか?
  • 継承チェーンはどうなっているか?

を、モジュールのネストも意識しつつ確認していけばよさそうです。

その他

こちらは何が出力されるか?というと、

class A
  CONST = "Hello A"

  class << self
    def name
      const_get(:CONST)
    end
  end
end

class B < A
  CONST = "Hello B"
end

puts B.name #=> Hello B

const_get は、selfに定義された定数を探索するので、Bクラスの持っているCONST Hello B が表示されます。

参考

Rubyの認定試験があったので受けてみた

最近、Rubyを原点に戻って学び直しているのですが、 その学習の一環として、Ruby技術者認定試験を受験することにしました。

試験は、javaoracleと同じような感じでレベル分けがされており、RubyではSilverとGoldの2段階がありました。

詳しくはこちら http://www.ruby.or.jp/ja/certification/examination/

Goldに認定されるには、Sliver,Gold試験両方に受からなければならないということもあり、最初に今回はSilverを受けてきました。

結果は合格で 88/100(75点合格)でした。

90点台はいけるかなと思っていたのですが、どこかのひっかけ問題にまんまと引っかかってしまっていたようです。

Silverはそんなに難易度は高くはありませんでしたが、落ちると受験料16,200円(税込)が吹っ飛ぶので、若干の良いプレッシャーを感じながら学習できました。

試験対策をして効果はあったのか?と聞かれればあった気がします。

私は試験対策がただの覚えゲーにならないよう、最初は試験範囲はあえて意識せず、以下の書籍を復習するようにしました。そのきっかけになったこと。

組み込みクラスのメソッドが試験対策前よりパッとでてくるようになったこと。割と前から知らなかったメソッドも多くありました。

あまり触る機会の無かった、RubyでIOクラス周りの理解が深まったこと。 これに関しては、ファイルをまとめて処理するバッチを作る場面などで役立ちそうな気がしています。

などなど。。。

次はGoldへと行きたいところです。 (でもphprailsもやり直したいな)

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 が付与されていることを確認

JWT(+JWS)について調べた (JWTの検証)

前回、こちらでHMAC SHA256で署名したJWT(+JWS)を作成していきましたが、今回はその検証を行っていきます。

takapi86.hatenablog.com

rfc7519 に検証手順があるので、それを参考にしつつ流れを確認しました。

  • JWTをheader、payload、signatureに分割
  • 分割したheader、payload、signatureをBASE64urlでデコード
  • headerから署名アルゴリズムを確認する
  • 確認した署名方法でJWTの検証を行なう

これに加えて有効期限の検証も行っていきます。

とにかくコードを書いていきます。 今回はコードの中で説明をしていく形になりますが、順番的にはこんな感じで検証していくのが良いかと思われます。

# -*- coding: utf-8 -*-

require 'base64'
require 'json'
require 'openssl'

jwt = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsImV4cCI6MTU0MDY1MjQwMH0.a2xzydhHuo2UREDRIdcyKmlihl_1gl_BYCxdQEKmgTE'

# JWTをheader、payload、signatureに分割

encoded_header, encoded_payload, encoded_signature = jwt.split('.')

# 分割したheader、payload、signatureをBASE64urlでデコード

decoded_header = Base64.urlsafe_decode64(encoded_header) # パディングがなくてもよしなにデコードしてくれる
decoded_payload =  Base64.urlsafe_decode64(encoded_payload)
decoded_signature = Base64.urlsafe_decode64(encoded_signature)

# headerから署名アルゴリズムを確認する
# 今回は形式はJWTでHS256以外のものは対応しないので、それ以外は処理しないようにします

header = JSON.parse(decoded_header)
payload = JSON.parse(decoded_payload)

unless header['typ'] == 'JWT' && header['alg'] == 'HS256'
  puts 'error: 対象外の形式です。'
  return
end

# 確認した署名方法でJWTの検証を行なう

secret = 'secret'

verification_signature = OpenSSL::HMAC.digest(
  OpenSSL::Digest::SHA256.new,
  secret,
  "#{encoded_header}.#{encoded_payload}"
)

if decoded_signature != verification_signature
  puts 'error: 検証が失敗しました。'
  return
end

# 有効期限を設けていれば、その検証も行なう

unless Time.now.to_i < payload['exp']
  puts 'error: 有効期限が過ぎています。'
  return
end

puts '検証OK'

参考

JWT(+JWS)について調べた

JWT とは

JSONオブジェクトで、安全に2者間で情報をやりとりするための表現方法です。 RFC7519で定義されています。

JWS とは

JSON Web Signatureの略、JWTのエンコード形式、デジタル署名、もしくはMACを行ったメッセージの表現 RFC7515で定義されています。 他には、暗号化するための仕様としてJSON Web Encryption (JWE)なんかがあります。

今回は、使われているケースが多いJWT+JWSの組み合わせのパターンでの実装を調べていきたいと思います。

特徴

  • コンパクトである
    • サイズが小さいので、URL、POSTパラメータ、またはHTTPヘッダー内で送信できます。
  • 自己完結されている
    • ペイロードには、ユーザーに関する必要な情報がすべて含まれているため、(使い方にもよりますが)データベースを複数回クエリする必要はありません。
    • ペイロードとは・・・やりとりしたい情報の本体(ヘッダなどの付加的情報が取り除かれたデータです。)
  • url-safeである
    • URLで使用できる文字列のみで構成されています。

どんなときに使うのか

  • 認証

    • こちらが一般的な使われ方のようです。
    • ユーザーがサービスにログインしJWTを発行してらもらい、その発行されたJWTで他に許可された異なるドメインのリソースを取得する。などの使い方ができます。
  • 情報の交換

    • JWTには、公開鍵/秘密鍵のペアを使用して署名することができるため、送信者は自分が誰であるかを確認できます。さらに、ヘッダーとペイロードを使用して署名が計算されるので、コンテンツが改ざんされていないことを確認することもできます。

OpenID ConnectやOAuthと組み合わせて使うケースもあるようです。

JWTの構造

これらをBase64エンコーディング(urlsafe、パディング無し)に変換し、.区切りで結合します。

こんな形になります。

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsImV4cCI6MTU0MDY1MjQwMH0.a2xzydhHuo2UREDRIdcyKmlihl_1gl_BYCxdQEKmgTE

ヘッダ

ヘッダには、このトークンのタイプ、使用されているハッシュアルゴリズムなどの情報で構成されます。

この例では、 ハッシュアルゴリズムが、HS256 (HMAC SHA-256アルゴリズムトークンのタイプが、JWT ということを示しています。

{
  "alg": "HS256",
  "typ": "JWT"
}

ペイロード

ペイロードは以下のような形式になります。 ペイロードにはクレームを含んでいます。 (クレームとは、エンティティ(通常はユーザー)と追加のメタデータのことを指します。)

クレームには、以下3種類があります。

  • 予約済み

    • 必須ではありませんが、事前定義された推奨のクレームです。例えば以下のようなものがあります。
      • iss(発行者)
      • exp(有効期限)
      • sub(サブジェクト)
      • aud(オーディエンス)
  • 公開済み

    • JWTを使用する人が自由に定義できます。 しかし、衝突を避けるためには、IANA JSON Webトークレジストリで定義するか、衝突抵抗ネームスペースを含むURIとして定義する必要があります。
  • 個人用

    • これらは、それらの使用に同意する当事者間で情報を共有するために作成されたカスタムクレームです。
{
  "sub": "1234567890",
  "name": "John Doe",
  "admin": true,
  "exp": 1540652400
}

有効期限(exp)は、 NumericDate である必要があるので、ここでは、UNIXTIMEを入れています。 https://tools.ietf.org/html/rfc7519#section-4.1.4

署名

先ほど、紹介したJOSE ヘッダ、JWSペイロード、(Base64urlエンコード済)を、 HMAC SHA-256 アルゴリズムで署名し, [JWS]に指定された形で署名をbase64urlエンコードすると, このエンコード済JWS署名を作り出せます。

RubyでJWTを作ってみる

# -*- coding: utf-8 -*-

require 'base64'
require 'json'
require 'openssl'

header = {
  alg: 'HS256',
  typ: 'JWT'
}

payload = {
  sub: '1234567890',
  name: 'John Doe',
  admin: true,
  exp: 1540652400
}

secret = 'secret'

encoded_header = Base64.urlsafe_encode64(header.to_json).delete('=')
encoded_payload = Base64.urlsafe_encode64(payload.to_json).delete('=')

signature = OpenSSL::HMAC.digest(
  OpenSSL::Digest::SHA256.new,
  secret,
  "#{encoded_header}.#{encoded_payload}"
)
encoded_signature = Base64.urlsafe_encode64(signature).delete('=')

jwt = [
  encoded_header,
  encoded_payload,
  encoded_signature,
].join('.')

puts '[jwt] => ' + jwt

結果

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsImV4cCI6MTU0MDY1MjQwMH0.a2xzydhHuo2UREDRIdcyKmlihl_1gl_BYCxdQEKmgTE

値の確認

こちらのjwt.ioというサイトのDebuggerツールで、値の確認ができます。

https://jwt.io/#debugger-io

JWTの検証

こちらは、別の記事でまとめていく予定です。

window.postMessage APIの使い方を調べた

postMessageとは

  • HTML5で拡張された機能
  • 任意のウィンドウへクロスオリジン通信が可能
  • セキュアである
  • 双方向の通信チャンネルが確立されてから通信を行う
    • 悪意あるWebサイトから知らないうちにされないようにしているため

使い方(送信)

win.postMessage("メッセージ", "http://www.example.com");

メッセージを受け取りたい対象に対してpostMessageを呼びます。 コードで示している win は以下を指します。

  • 自分のドキュメントウィンドウ内に生成されたiframe
  • JavaScriptで開いたポップアップウィンドウ
  • 自分のドキュメントウィンドウを含むウィンドウ
  • 自分のドキュメントを開いたウィンドウ

第一引数に渡したいメッセージ(文字列)、第二引数に宛先のオリジンを指定します。

受け取り方(受信)

addEventListenerを使って、messageイベントを登録します。

addEventListener('message', function(event) {
  // 受け取ってからの処理
}, false);

この event には、以下3つのプロパティが用意されています。

  • event.data
    • 実際のメッセージ内容(文字列)
  • event.origin
    • メッセージを送ってきたページの生成元
  • event.source
    • メッセージを送ってきた window オブジェクトの参照

セキュリティ上の注意点

postMessageの第二引数にワイルドカードを指定しない

postMessageの第二引数に '*' を指定すると、どのオリジンに対してもメッセージを送信することができるため、意図して使わない限り指定しないようにしたほうが良さそうです。

受け取ったメッセージの送信元を確認する

addEventListenerは送られてきたすべてのメッセージを受け取ることができるため、信頼できないページからのメッセージも受け取ることができます。 そのため、受け取ったら必ず以下のように信頼できる送信元であるかチェックをしたほうが良いです。

addEventListener('message', function(event) {
  if (event.origin !== 'http://www.example.com/') {
    return;
  }
}, false);

補足

最近は文字列でなくても良い

最新のブラウザではpostMessageの第一引数に、様々なデータオブジェクトを受け付けることができるようになっています。 ただ、IE8,IE9の場合はこの方法は使えないので、互換性を持たせたいのであればJSONを使うほうが良さそうです。 (もう、サポート切れているし意識しなくても良いかな。)

IE8以下は、addEventListener は使えないので attachEvent を使う

詳しくはこちら https://qiita.com/39_isao/items/506e08031adfca9a5933

参考