備忘録: RedmineのJSONP APIは安全か

はじめに

RedmineのJSONP APIを使って自作のWebアプリからRedmineにアクセスした。JSONPについては、ネットで調べるとセキュリティリスクについて書かれたものが多くあり、中には「絶対に使うべきではない」と書かれているものもある。「果たして使って大丈夫なのか?」と気になって調べてみた。

結論としては「大丈夫」という判断をしたが、その検討結果について今後のために備忘録として残すことにする。ただしセキュリティは奥深く筆者は素人なので、もしこの記事を読まれる方がいたら、あくまで参考に留めて頂きたい。

なぜJSONPを使うのか?

RedmineにアクセスするAPIには、RESTとJSONPの2つの方式が用意されている。JSONPは読み取り (GET) のみ、RESTはそれに加えて書き込み (POST/PUT) も削除 (DELETE) もできる。ならばRESTを使えば良さそうだがそう単純ではない。

RESTでアクセスするには、今回の構成では後述するCORS (Cross-Origin Resource Sharing) のサポートがRedmine側に必要になる。CORS対応には追加の構成が必要なため、世の中の大半のRedmineサーバーは、CORS対応になっていない (と思われる)。このためCORSを前提にすると、自作Webアプリ展開のハードルが高くなってしまう。

自作Webアプリがデータの読み込みしか必要としないのであれば、JSONPを使うことで自作Webアプリ導入のハードルを下げることができる。これがJSONPを使用した理由である。

SOP (Same Origin Policy) について

JSONPについて検討するには、まずSOPについて知っておく必要がある。SOPとは複数のドメインにまたがるデータの流れを禁ずるブラウザの仕様である。(ポリシーだから仕様の元になった考え方というべきか …)

今回の自作Webアプリの場合は、下記のような構成がとられている。

図1 システム構成

やりたいことは次のようなシナリオである。ブラウザにサーバーAから自作Webアプリのスクリプトを読み込む (#1)。そこからRedmineサーバーにアクセス (#2) してデータを取得する (#3)。更にスクリプトからこのデータをサーバーAに保管 (#4) する。つまりサーバーAからRedmineサーバーのデータにアクセスすることになる。

もしこの自作Webアプリのスクリプトが、悪意のスクリプトであった場合、データ漏洩が発生する。ブラウザではスクリプトの意図 (悪意) を判別することができない。そのため、このようなドメインをまたがったアクセスについて、セキュリティリスクが高いとして、ブラウザは原則これを禁止している。上図で言えば#3のレスポンスがブラウザにより遮断される。これがSOPである。

JSONPとは

JSONPは、SOP要件を回避するために考案された手法である。

スクリプトの読込については、他ドメインへのアクセスであっても、SOPは適用されない。これを利用して、サーバーからデータを読み込んでいる。

自作WebアプリからRedmineのJSONPエンドポイントをスクリプトとして読み込む (図1の#2)。これに対して返されるスクリプト (同#3) には、下記のようにコールバックの呼び出しが記述されている。また呼び出しの引数には、サーバーから返されるデータが設定されている。

callbackA({x: ‘data1’, y: ‘data2’, z: ‘data3’})

自作Webアプリには、このcallbackAを関数として予め用意しておく。#3のスクリプトが読み込まれると、JavaScriptとして実行され、callbackAに対する呼び出しが発生する。その結果、用意したcallbackAが呼び出され、引数として渡されたデータを受け取ることができる。受け取ったデータをサーバーAに送信 (#4) すれば元のシナリオが完成する。

JSONPのセキュリティリスク

JSONPについては、セキュリティリスクを指摘するサイトが多い。中には一律使用禁止を唱えるサイトもある。

そのようなサイトで論じられているのは、JSONPのコールバック関数名が推測または指定可能である、ということである。コールバック関数名が判明すれば、悪意のあるスクリプトは、自前のコールバック関数を用意することで、APIから返されたデータを自由に受け取ることができる。コールバック関数名の指定には幾つかの方法があるが、総当りなどの方法でコールバック関数名を特定できる可能性がある。

ただしこの議論は、JSONP APIの保護がコールバック関数名の秘匿によって行われる、ということを前提にしている。しかし後述するように別の認証と組み合わせることで、これに対処することができる。

なおJSONPのセキュリティリスクには、上述のような呼び出される側のリスクの他に、逆に悪意のあるサイトにJSONPでアクセスすることで、悪意のあるスクリプトを送り込まれるという、呼び出す側のリスクも存在する。これについては本稿の検討対象外とする。これはアクセスするRedmineサーバーは、既知で信頼できるものという前提による。

JSONPのセキュリティリスクの回避

コールバック関数名の秘匿による保護は効果がないため、別途APIに認証をかけることでセキュリティリスクを回避する。これはJSONPに限るわけではない。例えばREST APIの場合もこのような保護は必要になる。

Redmineの場合は、APIキーと呼ばれるトークンを呼び出しに含めることで認証を行っている。Redmine側では、JSONPの呼び出しに含まれるAPIキーが、自身で発行したものかどうかチェックして、リクエストを受け付けるかどうか判断している。APIキーはURLパラメーター、またはカスタムヘッダーに含めることができる。

このようなトークンを利用した認証はセキュリティ的に有効とされている。例えばOWASP (Open Web Application Security Project) のCross-Site Request Forgery Prevention Cheat Sheetには、CSRF (Cross-Site Request Forgery) Tokenとして紹介されている。ただしトークンの要件として下記の項目が挙げられている。

  • Unique per user session (ユーザーセッションごとに異なる)
  • Secret (秘匿される)
  • Unpredictable (推測不能である)

RedmineのAPIキーは、上記の2番目、3番目は満たしているが、再生成されない限り固定のため、1番目は満たしていない (それでもユーザー間でシェアされることはない)。このためOWASPの想定よりはセキュリティ強度が下がると考えるべきであろう。

APIキーをどのようにWebアプリに渡すのか、またWebアプリではそれをどう管理するのか、その方法によってはセキュリティリスクになる可能性がある。(秘匿性が破られる可能性)

CORSについて

SOPを回避するもうひとつの方法がCORSである (こちらが王道というべき)。REST APIを使用する場合は、CORSのサポートが必要になる。

CORSでは、アクセスされる側のサーバーが、他ドメインからのアクセスを受け付けることを宣言することで、ブラウザはSOP要件を解除する。この宣言は、アクセスされるサーバーからのレスポンスヘッダーにCORSヘッダーを含めることで行う。

ただしこの宣言は通常は条件付きである。その条件はCORSヘッダーに含まれる。アクセス元が限定できる場合は、”access-control-allow-orign”で指定すれば良い。ただし指定できるのは、単一のドメインまたはワイルドカードとなる。アクセス元が一つの場合は良いが、複数の場合はワイルドカードにせざるを得ない。

このためCORSとは別に認証の仕組みが必要となる。Redmineの場合は、APIキーを使って認証を行う。これはJSONPのときと同じである。

Redmineはそれ自体はCORSに対応していない。Redmineと組み合わされるWebサーバーでCORSヘッダーを生成し、レスポンスに付加してやる必要がある。

終わりに

冒頭に書いた通り「RedmineのJSON APIは使っても (使わせても) 大丈夫なの?」という疑問 (心配) が出発点。JSONPの使い方について調べ始めたら、やたらセキュリティリスクを指摘する記事が出てきたので。

コールバック関数に絡めてセキュリティリスクを指摘する記事は、筆者が目にした限りでは古い (2010年以前 …) ものが多い。その指摘は尤もだけど、適切な認証なしにAPIを公開する前提なのが、そもそも無理筋なのではとも思える。 JSONPが世に出た当時はそんなものだったということか …

新しい記事は、サーバー側から不正なスクリプトを送り込まれる、XSSのリスクを指摘するものがほとんどのようだ。これについては、アクセス先のRedmineサーバーが「既知で信頼できる」という前提の元に、リスクは生じないと判断した。

Redmineの開発者達もその辺りの議論をした上で、JSONPのAPIを提供することにしたものと推察したが …

変更履歴

日付内容
2023/09/16「終わりに」を追記
2022/09/23初版リリース