7
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【対策Gem付】GASスクリプトにAPIキーをハードコードするな

7
Last updated at Posted at 2026-02-13

TL;DR

  • APIキーやWebhook URLをコード内にハードコードすると、デプロイした際にリスクがあるよ
  • 機密情報はスクリプトプロパティを使ってね
  • GASでのセキュリティ原則を定義したGemを作ったよ

はじめに

昨年から今の会社に入社し、PdMとして自社プロダクト開発にいそしんでいますが、一時的な社内ツールとしてGAS(Google App Script)を使用して社内ツールや社内サイトを作ることが度々あります。
以前新卒社員にGASを使用した社内ツールの構築を依頼した際に、思わず2度見してしまった「APIキーやWebhookのハードコード」について、戒めも込めて書き連ねておきます。

やってはいけないハードコード

世の中の社内プロダクトを見ると、製品版ではありえないレベルのハードコーディングにあふれています。
当然ながらそのあたりを学んだことのない非IT技術者が作っているケースもありますし、初心者エンジニアがうっかりやってしまったケースもあるでしょう。
個人的には「ハードコード」を見つけた時に一概に指摘するのもどうかなと思っています。
時には学習の一環として、運用時に困ることに自分で気づくことも大事ですし。
ただ、APIキーやWebhookなどの下手をすればセキュリティに関わる部分は話が別です!

よくやるケース

よくやってしまうケースをGeminiに作ってもらいました

使ったプロンプト: GASのスクリプトを使用してSlackのIncoming Webhookにメッセージを送るスクリプトを作成してください。

出来上がったスクリプト

function sendSlackMessage() {
  // 1. Slackから取得したIncoming Webhook URLをここに貼り付けてください
  const webhookUrl = "https://hooks.slack.com/services/xxxxx/xxxxx/xxxxx";

  // 2. 送信するメッセージの内容
  const message = {
    "text": "これはGASから送信されたテストメッセージです。\n改行も可能です。👻",
    // 必要に応じて送信者名やアイコンも変更できます(Webhookの設定によります)
    // "username": "GAS通知Bot",
    // "icon_emoji": ":robot_face:" 
  };

  // 3. HTTPリクエストのオプション設定
  const options = {
    "method": "post",
    "contentType": "application/json",
    "payload": JSON.stringify(message)
  };

  try {
    // 4. Slackに送信実行
    UrlFetchApp.fetch(webhookUrl, options);
    Logger.log("Slackへの送信が完了しました。");
  } catch (e) {
    Logger.log("エラーが発生しました: " + e.toString());
  }
}

このレベルの雑なプロンプトでも最近のAIではしっかりと動くGASスクリプトを作ってくれます。非常にありがたいですね!
ただ、Incoming Webhook URLをコード内にハードコードさせる仕様となっています。

ハードコードすると何がダメなのか

GASにてこのハードコードが危険性を増すのは「デプロイ」を実施した時です。
ウェブサイトとしてGASで構築したサイトを公開したりする場面でデプロイを行いますが、この際に構築したWebアプリには設定次第で自分以外のアクセスを許可しています。
社内サイトなどとしてデプロイした場合は当然ながらそのような設定になっているでしょう。

この際、当然ながらブラウザの検証ツールでは、実行されているHTMLなどを参照することが可能です。
今回例示したSlackのメッセージ送信についても下記のようなケースでハードコードされたWebhook URLなどを閲覧されるリスクがあります。

  • HTML内(ボタンやリンクの遷移先)などでハードコードしている
  • .gsファイルにハードコーディングされており、ネットワークタブで閲覧可能な状態になっている

行うべき対策

GASではこうした事態に備えられるよう、プロパティサービスという機能が標準搭載されています。
image.png

GASの左側にあるサイドメニューから「プロジェクトの設定」を選択すると「スクリプトプロパティ」などのプロパティサービスを利用することが可能です。
ここにWebhook URLやAPIキーなどを記載しておき、.gsファイル側でそれを読みだすことで、安全に機密情報を扱うことができます。

社内に対策を普及させるには

Geminiには幸い「Gem」という便利な機能がありますので、こちらのカスタム指示でセキュリティ原則を守ってもらえるように教育しましょう。

カスタム指示例

## Google App Script(GAS)開発におけるセキュリティ原則

GASのスクリプトを作成する際は、以下のセキュリティ対策を常に順守してください。

1. **機密情報の秘匿化**: APIキー、Webhook URL、トークン、個人情報などの機密情報は、コード内に直接記述(ハードコード)しないでください。必ず`PropertiesService.getScriptProperties()`を利用して取得するコードを書き、設定が必要なプロパティ名のリストを付記してください。
2. **権限の最小化(`@OnlyCurrentDoc`)**: コンテナバインドスクリプトの場合、ファイル全体への不要なアクセスを避けるため、必要に応じて`// @OnlyCurrentDoc`アノテーションをスクリプトの先頭に含めてください。
3. **マニフェストの最適化(`appscript.json`)**: 特定のスコープ(oauthScopes)の制限が必要な場合や、Webアプリの設定が必要な場合は、`appscript.json`の適切な定義例を提示してください。
4. **XSS/インジェクション対策**: `ContentService.createTextOutput()`を用いてJSONなどのレスポンスを返す際は、入力値のエスケープや適切な`MimeType`の設定を行い、悪意あるスクリプトが混入(XSS等)しないような構造にしてください。
5. **Webアプリのオリジン検証**: `doPost`や`doGet`で外部リクエストを受ける場合、リクエストのパラメータやヘッダーをチェックし、意図しないドメインや不正なソースからのリクエストを拒否または検証するロジック(トークン検証など)を組み込んでください。

試してみた

先ほど作成したGemにて同じようにGASのスクリプトを作成してみました。

使ったプロンプト: GASのスクリプトを使用してSlackのIncoming Webhookにメッセージを送るスクリプトを作成してください。

出来上がったスクリプト

/**
 * Slackにメッセージを送信する関数
 * @param {string} message - 送信するメッセージ本文
 */
function postSlackMessage(message) {
  // セキュリティ対策: 入力値の検証(空文字や型チェック)
  if (!message || typeof message !== 'string') {
    console.error('エラー: 有効なメッセージ文字列が指定されていません。');
    return;
  }

  // セキュリティ対策: 機密情報の秘匿化
  // Webhook URLはコード内に記述せず、スクリプトプロパティから取得する
  const scriptProperties = PropertiesService.getScriptProperties();
  const webhookUrl = scriptProperties.getProperty('SLACK_WEBHOOK_URL');

  if (!webhookUrl) {
    throw new Error('設定エラー: スクリプトプロパティに "SLACK_WEBHOOK_URL" が設定されていません。');
  }

  // ペイロードの作成
  const payload = {
    text: message
  };

  const options = {
    method: 'post',
    contentType: 'application/json',
    payload: JSON.stringify(payload),
    muteHttpExceptions: true // エラーレスポンスの内容を確認するためにtrue設定
  };

  try {
    const response = UrlFetchApp.fetch(webhookUrl, options);
    const responseCode = response.getResponseCode();

    // レスポンスコードのチェック
    if (responseCode === 200) {
      console.log('Slackへの送信に成功しました。');
    } else {
      console.error(`Slackへの送信に失敗しました。コード: ${responseCode}, 内容: ${response.getContentText()}`);
    }
  } catch (e) {
    console.error(`通信エラーが発生しました: ${e.message}`);
  }
}

/**
 * テスト実行用関数
 */
function testSlackPost() {
  postSlackMessage('これはGASからのセキュアなテスト送信です。');
}

セキュリティ面を考慮したGASコードが作れていますね。
皆さんもセキュリティを担保しながら豊かなGASライフを歩んでいきましょ~!

さいごに

MSOL Digitalでは、ミッション・ビジョンの実現に向けて、新たな仲間を募集しています。
 少しでもご興味をお持ちいただけましたら、ぜひエントリーをご検討ください。
また、すぐに転職をお考えでない場合でも、カジュアル面談からのスタートも大歓迎です。
まずはお気軽にお話しできますと幸いです!

■エントリーページ

■カジュアル面談の申し込み

7
4
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
7
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?