App Proxy(Shopify App Proxy)
定義
App Proxyは、ECサイトのドメイン配下のパスへのアクセスを、Shopifyを経由して自分のアプリのサーバーへ転送する機能。買い物客のブラウザに見えているのは、いま見ているECサイトと同じドメイン(たとえば my-brand.com)上のURLのままなので、CORSやサードパーティCookieの制約を受けずに外部のデータを表示できる。
転送のされ方
レビュー表示アプリで、転送先を https://reviews.example.com/proxy、prefixを apps、subpathを reviews と宣言した場合:
買い物客がアクセスするURL
https://my-brand.com/apps/reviews/list?product_id=123
アプリのサーバーが受け取るURL
https://reviews.example.com/proxy/list?product_id=123&shop=my-brand.myshopify.com&logged_in_customer_id=456&path_prefix=%2Fapps%2Freviews×tamp=1789200000&signature=…
パスのうち /apps/reviews が転送先のURLに差し替わり、その先の /list とクエリパラメータはそのまま引き継がれる。そこにShopifyが4つのパラメータと署名を足す。
設定
shopify.app.toml の [app_proxy] に、転送先URL、prefix、subpathを書く。prefixは a apps community tools のいずれか。write_app_proxy スコープが必要で、アプリ1つにつき1経路だけ持てる。
Shopifyが付ける情報
shop、logged_in_customer_id、path_prefix、timestamp、signature の5つ。
signature の検証は、それを除いたクエリパラメータを並べ替えて連結し、アプリのクライアントシークレットを鍵にHMAC-SHA256を計算して突き合わせる。Webhookの署名がBase64でヘッダーに入るのに対し、こちらは16進数でクエリパラメータに入る。
応答の作り方
Cookieは使えない。リクエストの Cookie ヘッダーとレスポンスの Set-Cookie はShopifyが取り除くため、Cookieでセッションを持つ設計は成立しない。
レスポンスの Content-Type を application/liquid にすると、ストアのテーマの文脈でLiquidが描画される。
背景
なぜ使うのか
App Proxyを使う動機は、ECサイトのドメインのまま自前のバックエンドを呼べることにある。上の例で言えば、買い物客のブラウザに表示されているURLは my-brand.com のままで、実際に応答しているのは自社のサーバーになる。
別ドメインのAPIを直接叩く構成に比べ、CORSやサードパーティCookieの制約から解放される。ユーザーから見てもストアの一部として自然に見える。
署名が証明すること、しないこと
この機能で最も注意が要るのは、署名の検証だけでは足りないという点になる。
署名はShopifyが転送する時点で、その時点のクエリパラメータ全体に対して計算される。買い物客のブラウザが付けたパラメータも、Shopifyが足すパラメータと区別なく署名の対象に入る。つまりログイン中の利用者が ?customer_id=999 のように任意の値を付けてアクセスすれば、Shopifyはその値ごと署名して転送する。アプリに届く署名は正しく、検証は問題なく通る。
したがって署名が保証するのは、Shopifyを経由して届いたことと、経路上で改ざんされていないことまで。パラメータの値が正当であることは保証しない。値を決めたのは利用者だからだ。
信用してよい身元は1つだけ
リクエストの中で身元として信用してよいのは logged_in_customer_id だけになる。これはShopifyがログインセッションを見て付ける値で、利用者側から指定できない。
逆にアプリが自前の customer_id のようなパラメータを見て、誰のデータを返すかを決めているとどうなるか。ログインさえしていれば、他人のIDを入れたリクエストが正しい署名付きで通り、他人の注文履歴や個人情報がそのまま返る。アクセス制御の不備としてはよくある形で、公式ドキュメントもSession Tokenの sub クレームは信用できるが ?customer_id= は信用できない、と明記している。
対策は2つ。要求されたデータの持ち主が logged_in_customer_id と一致するかをアプリ側で必ず確認すること。そして、ログインしていない場合この値は空文字になるので、空を認証されていないものとして扱い、既定値を当てて処理を続けないこと。
リプレイを防ぐ
署名には timestamp も含まれるため改ざんはできないが、有効期限の概念はない。正当なリクエストを丸ごと記録して後から何度も送る攻撃を防ぐには、アプリ側でタイムスタンプが十分新しいかを検証する必要がある。突き合わせ自体も、タイミング攻撃に強い比較関数を使う。
ログに残る接続元はShopify
サーバーのアクセスログに接続元として記録されるのは、買い物客ではなくShopifyのサーバーになる。TCP接続を張ってくるのがShopifyだからだ。元のリクエスト元のIPアドレスは X-Forwarded-For ヘッダーに、買い物客が実際に開いていたドメインは X-Forwarded-Host に入って渡される。
ログ分析、地域判定、不正アクセスの追跡でクライアントIPを使うなら、接続元アドレスではなくこのヘッダーを読む。接続元アドレスのまま数える実装だと、すべてのアクセスが少数のShopifyのアドレスに集約されて見える。
ただしヘッダーは誰でも付けられる。X-Forwarded-For を信用してよいのは、そのリクエストが本当にApp Proxyを通ってきたと署名で確認できた場合に限られる。
レート制限は二段構え
App Proxyのエンドポイントに届くリクエストは、まずShopify側のインフラを通る。そのためShopifyがホストするストアフロントのページに働く保護が、ここでも効く。Shopifyは署名のない自動化トラフィックに厳しいレート制限を課しており、悪質と判断したリクエストには 430 Shopify Security Rejection を返す。バックエンドを直接インターネットに晒す構成と比べれば、この一段は確実に有利に働く。
一方で、しきい値が公開されているわけでも、アプリ側から調整できるわけでもない。プラットフォーム側の保護は土台として扱い、業務上必要なレート制限はアプリ側で別途実装する。その際、接続元アドレスで絞る実装はShopifyを絞ることになって機能しないため、X-Forwarded-For か logged_in_customer_id を単位にする。
重要な制約
- prefixとsubpathはマーチャントが変更できる: 管理画面から変更でき、変更は即座に反映される。アプリ側で固定値として扱わず、
path_prefixを見て判断する。なおアプリ側で宣言を変えても、既存インストールには反映されず新規インストールだけに効く - UI Extensionから呼ぶ場合は条件が変わる: Checkout UI ExtensionとCustomer Account UI Extensionのどちらからも呼べるが、実行環境がサンドボックス化されたWeb Workerになるため前提が変わる。
network_accessの宣言(Partner Dashboardでの申請が要るが自動承認される)が必要で、リクエストはCORSになる。Extensionのoriginは予告なく変わりうるため、ShopifyはAccess-Control-Allow-Origin: *を返すことを公式の要件としている。実際にはチェックアウトのホストなどに絞っても動作すること自体は当社で確認しているが、将来ホストが変わりうるというのが*を指定している理由なので、公開するアプリではこれに従う。さらにlogged_in_customer_idは付かないので、ログイン中の顧客はSession Tokenのsubクレームで判定する - パスワード保護中のストアでは、UI Extensionからのこの経路は利用できない: ExtensionはWeb Workerから動き、親ウィンドウとセッションを共有しないため
- 扱えないHTTPメソッドがある:
CONNECTとTRACEは非対応
Flagshipでの関わり
自社で公開しているShopifyアプリのMypage Blocksをはじめ、ストアフロントから自社バックエンドを呼ぶ構成でApp Proxyを採用している。
実装では署名の検証に加えて、リクエストに含まれるIDと logged_in_customer_id の突合確認を必須の要件としている。署名が通っていることと、そのデータを見てよい相手であることは別だという前提を、実装でもレビューでも共有している。タイムスタンプの妥当性検証もリプレイ対策として明示的な作業項目に置く。大量リクエストへの対策はApp Proxyの責務ではないと整理したうえで、必要な経路にはアプリ側の制限を実装する方針を採る。
署名不一致の調査では、シークレットの取り違え、想定と異なるアプリにApp Proxyが設定されている、プロキシを経由せずバックエンドへ直接リクエストが来ている、署名時と検証時でクエリパラメータが変わっている、という切り分けの型を持っている。
社内では、Shopifyのシステムを扱ううえで必要なセキュリティ知識を揃えるための教育を進めており、App Proxyの署名検証はその項目のひとつになっている。