Google Apps Script (GAS) で Slackbot や連携ツールを作るとき、
避けて通れないのが「Webアプリの公開設定」です。
Slackからのイベントを受け取るには、アクセス権限を「全員 (Anyone)」にする必要があります。
つまり、URLさえバレれば誰でもスクリプトを実行できてしまう状態です。
Slack公式が推奨する「Signing Secret (署名検証)」を実装しようにも、GASの仕様上HTTPヘッダーが取得できず実装不可能です。かといって、非推奨(Legacy)のVerification Tokenは使いたくない。
そんな課題を解決するために、Google Cloud Functions (GCF) を「認証プロキシ」として配置するアーキテクチャを構築しました。その実装ノウハウを共有します。
課題:GAS単体では「署名検証」ができない
SlackからのWebhookリクエストが「正当なSlackからのものか」を検証するために、通常はHTTPヘッダーに含まれる x-slack-signature を使用します。
しかし、GASの doPost(e) イベントオブジェクトには、HTTPリクエストヘッダーを取得する機能がありません。
これにより、以下のセキュリティリスクを抱えることになります。
- なりすまし: URLを知っていれば誰でもリクエストを送れる。
- リプレイ攻撃: 通信を傍受された場合、そのリクエストを再送されてしまう。
解決策:Cloud Functionsをプロキシにする
GASの前段に、HTTPヘッダーを扱える Cloud Functionsを配置し、そこで署名検証を行う構成を採用しました。
アーキテクチャ
Slack ➡ GCF ➡ GAS
この構成のポイント
- Slack ➡ GCF: Slack推奨のSigning Secretで検証。
- GCF ➡ GAS: 独自のHMAC-SHA256署名を付与して転送。
- GAS側: IP制限はできないが、GCFが付与した署名を検証することで、実質的にGCF経由以外のアクセスを遮断。
実装の詳細
1. Cloud Functions
GCF側では、以下の2つの責務を担います。
- Slackからのリクエストヘッダー (
x-slack-signature) を検証する。 - 検証OKなら、GASとの共通鍵 (
PROXY_SECRET) でペイロードを署名し、GASへ転送する。
// index.js (抜粋)
const functions = require('@google-cloud/functions-framework');
const crypto = require('crypto');
functions.http('slackProxy', async (req, res) => {
// 1. Slackの署名検証 (Signing Secret)
if (!verifySlackSignature(req)) {
return res.status(401).send('Verification failed');
}
// 2. GASへ転送するためのプロキシ署名を作成
// リプレイ攻撃防止のためタイムスタンプを含める
const timestamp = Math.floor(Date.now() / 1000).toString();
const bodyString = JSON.stringify(req.body);
const sigBasestring = `v0:${timestamp}:${bodyString}`;
// 共通鍵(PROXY_SECRET)でHMAC署名を作成
const signature = crypto
.createHmac('sha256', process.env.PROXY_SECRET)
.update(sigBasestring, 'utf8')
.digest('hex');
// GASへPOST (署名はクエリパラメータで渡す)
const gasUrlObj = new URL(process.env.GAS_URL);
gasUrlObj.searchParams.append('signature', signature);
gasUrlObj.searchParams.append('timestamp', timestamp);
const response = await fetch(gasUrlObj.toString(), {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: bodyString,
});
// ...レスポンス処理
});
2. Google Apps Script (GAS)
GAS側では、doPost の冒頭でGCFから送られてきた署名を検証します。
GAS標準の Utilities.computeHmacSha256Signature を使うことで、外部ライブラリなしで高速に処理可能です。
// main.gs (抜粋)
const proxySecret = PropertiesService.getDocumentProperties().getProperty("ProxySecret");
function doPost(e) {
// プロキシ署名の検証
if (!verifyProxySignature(e)) {
console.error("不正なアクセスです");
return ContentService.createTextOutput("Error: Unauthorized");
}
// ...メイン処理...
}
function verifyProxySignature(e) {
const signature = e.parameter.signature;
const timestamp = e.parameter.timestamp;
const bodyString = e.postData.getDataAsString();
// 1. タイムスタンプ検証 (5分以内) -> リプレイ攻撃対策
const now = Math.floor(new Date().getTime() / 1000);
if (Math.abs(now - Number(timestamp)) > 300) return false;
// 2. 署名の再計算と照合
const sigBasestring = `v0:${timestamp}:${bodyString}`;
const computedSignature = Utilities.computeHmacSha256Signature(sigBasestring, proxySecret);
// Byte配列をHex文字列に変換して比較
const computedHex = computedSignature.reduce((str, byte) => {
const hex = (byte < 0 ? byte + 256 : byte).toString(16);
return str + (hex.length === 1 ? "0" : "") + hex;
}, "");
return signature === computedHex;
}
まとめ
GASの「手軽さ」を残しつつ、セキュリティ要件を満たすためにCloud Functionsをプロキシとして挟む構成は非常に有効でした。
- Slack公式推奨 (Signing Secret) に準拠できた。
- 非推奨機能 (Verification Token) を廃止できた。
- GAS特有の制約を HMAC署名リレー で安全に回避できた。
同じような構成で悩んでいる方の参考になれば幸いです。