Webアプリの入力値を保存してから困る前に|GASで文字列をdry-run検査

Webアプリの入力値を保存してから困る前に|GASで文字列をdry-run検査

ツールおじ|GoogleアカウントをAIエージェント化

ツールおじ|GoogleアカウントをAIエージェント化

Webアプリのフォームを作ると、空欄や長すぎる文章、画面表示で意味が変わる記号がそのまま届くことがあります。保存処理へ進む前に、入力値の形式と長さを確認し、表示用の文字列へ変換できる無料GASを用意します。

この例はシートへ書き込まず、メールも送りません。まずサンプル入力だけで検査結果を確認するためのdry-runです。

先に知っておきたいこと

  • できること:必須項目、文字数、公開範囲の選択値、制御文字、表示前に変換したい記号を確認できます。
  • できないこと:利用者認証、権限管理、レート制限、CSRF対策、URLやCSSなど文脈別の安全性までは保証しません。
  • 必要なもの:Google Apps Scriptの新規プロジェクトだけです。Webアプリとして公開しなくてもテストできます。
  • 初期状態では `dryRun: true` です。保存、送信、外部公開は行いません。

今回確認する入力

フォームから届く想定の値を、次の3項目に絞ります。

  • `displayName`:表示名。1〜40文字
  • `summary`:説明。1〜200文字
  • `publicScope`:公開範囲。`PRIVATE` または `TEAM` のみ

実際の氏名、メールアドレス、顧客情報ではなく、最初は架空のサンプルで試してください。

コピーして使える最小コード

function auditWebAppInputFree() {
  const config = {
    dryRun: true,
    allowedScopes: ['PRIVATE', 'TEAM'],
    maxLengths: {
      displayName: 40,
      summary: 200,
    },
  };

  // 実データではなく、最初は架空の入力だけで確認します。
  const sampleInput = {
    displayName: '  サンプル利用者  ',
    summary: '確認用 <script>alert(1)</script> の文字列です。',
    publicScope: 'TEAM',
  };

  if (config.dryRun !== true) {
    throw new Error('安全のため、無料例はdryRun=trueでのみ実行できます。');
  }

  const result = validateAndSanitizeWebInput_(sampleInput, config);

  // 入力本文はログへ出さず、件数と判定コードだけを記録します。
  console.log(JSON.stringify({
    dryRun: true,
    valid: result.valid,
    issueCount: result.issues.length,
    issueCodes: result.issues.map(function(issue) { return issue.code; }),
    changedFieldCount: result.changedFields.length,
  }));

  return result;
}

function validateAndSanitizeWebInput_(input, config) {
  if (!input || Object.prototype.toString.call(input) !== '[object Object]') {
    throw new Error('入力はオブジェクト形式で指定してください。');
  }

  const requiredFields = ['displayName', 'summary', 'publicScope'];
  const issues = [];
  const cleaned = {};
  const changedFields = [];

  requiredFields.forEach(function(field) {
    if (!Object.prototype.hasOwnProperty.call(input, field)) {
      issues.push({ field: field, code: 'MISSING_FIELD' });
    }
  });

  if (issues.some(function(issue) { return issue.code === 'MISSING_FIELD'; })) {
    return {
      valid: false,
      issues: issues,
      changedFields: changedFields,
      preview: null,
    };
  }

  const displayName = normalizeText_(input.displayName);
  const summary = normalizeText_(input.summary);
  const publicScope = normalizeText_(input.publicScope).toUpperCase();

  if (!displayName) {
    issues.push({ field: 'displayName', code: 'EMPTY_VALUE' });
  }
  if (!summary) {
    issues.push({ field: 'summary', code: 'EMPTY_VALUE' });
  }
  if (displayName.length > config.maxLengths.displayName) {
    issues.push({ field: 'displayName', code: 'TOO_LONG' });
  }
  if (summary.length > config.maxLengths.summary) {
    issues.push({ field: 'summary', code: 'TOO_LONG' });
  }
  if (config.allowedScopes.indexOf(publicScope) === -1) {
    issues.push({ field: 'publicScope', code: 'INVALID_SCOPE' });
  }
  if (containsControlCharacters_(displayName) || containsControlCharacters_(summary)) {
    issues.push({ field: 'text', code: 'CONTROL_CHARACTER' });
  }

  cleaned.displayName = escapeHtmlText_(displayName);
  cleaned.summary = escapeHtmlText_(summary);
  cleaned.publicScope = publicScope;

  if (cleaned.displayName !== String(input.displayName)) {
    changedFields.push('displayName');
  }
  if (cleaned.summary !== String(input.summary)) {
    changedFields.push('summary');
  }
  if (cleaned.publicScope !== String(input.publicScope)) {
    changedFields.push('publicScope');
  }

  return {
    valid: issues.length === 0,
    issues: issues,
    changedFields: changedFields,
    // previewは画面の本文テキストとして表示する場合の候補です。
    // HTML属性、URL、JavaScript、CSSへはそのまま使わないでください。
    preview: issues.length === 0 ? cleaned : null,
  };
}

function normalizeText_(value) {
  if (value === null || value === undefined) return '';
  return String(value).normalize('NFKC').trim();
}

function containsControlCharacters_(value) {
  // タブ・改行以外の制御文字を検出します。
  return /[\u0000-\u0008\u000B\u000C\u000E-\u001F\u007F]/.test(value);
}

function escapeHtmlText_(value) {
  return value
    .replace(/&/g, '&amp;')
    .replace(/</g, '&lt;')
    .replace(/>/g, '&gt;')
    .replace(/"/g, '&quot;')
    .replace(/'/g, '&#39;');
}

実行手順

  1. Google Apps Scriptで新しいプロジェクトを作ります。
  2. 初期のコードを上のコードへ置き換えます。
  3. `auditWebAppInputFree` を選び、実行します。
  4. 実行ログで `dryRun`、`valid`、`issueCount`、`issueCodes`、`changedFieldCount` を確認します。
  5. `sampleInput` の空欄、文字数、`publicScope` を少しずつ変え、想定したエラーになるか確認します。
  6. 実際のフォームと接続する前に、コピーしたテスト用プロジェクトで境界値を確認します。

初回はApps Scriptの実行許可が求められる場合があります。内容を確認できない権限要求が出た場合は、そこで止めてください。

ログの見方

このコードは入力本文をログへ出しません。たとえば `MISSING_FIELD` があれば項目不足、`TOO_LONG` があれば文字数超過、`INVALID_SCOPE` があれば許可していない公開範囲です。

`changedFieldCount` は、前後の空白除去、文字の正規化、HTML本文向け変換などで値が変わった項目数です。変換後の本文そのものはログへ残さないため、個人情報を誤って実行ログへ広げにくくしています。

失敗しやすい点と制限

  • HTML本文向けの変換だけで、Webアプリ全体の安全性が完成するわけではありません。
  • HTML属性、リンク先URL、CSS、JavaScriptへ値を埋め込む場合は、それぞれ別の検証が必要です。
  • 公開範囲、実行ユーザー、アクセス権は、Webアプリをデプロイする前に別途確認してください。
  • 利用者認証や権限判定はクライアント側だけに置かず、サーバー側でも確認してください。
  • 大量アクセス対策、連続送信制限、保存先の権限、エラー時の再実行設計はこの無料例の範囲外です。
  • `preview` が `null` の場合は保存処理へ進めず、入力画面へエラーコードを返す設計にしてください。

公開前チェックリスト

  • dry-runを解除する前に、必須項目と最大文字数を固定したか
  • 許可する公開範囲を一覧で管理し、想定外の値を拒否しているか
  • 入力本文や個人情報をログへ出していないか
  • 表示先の文脈に合う変換・検証を行っているか
  • 保存、送信、外部API呼び出しの前に検査エラーで停止できるか
  • Webアプリの実行ユーザーとアクセス権を確認したか
  • 少量の架空データで正常系・空欄・上限超過・想定外値を試したか

まとめ

Webアプリの入力事故は、保存処理を作る前に「受け取ってよい形」を決めると減らしやすくなります。まずはデプロイも保存もしないdry-runで、必須項目、長さ、許可値、ログ内容を確認してください。

公開前の入力検証や確認工程を、商品準備全体のチェックへ広げたい方はこちらです。

GasおじのTips商品一覧を見る

※このコードは学習・検証用の最小例です。実データへ適用する前にコピー環境で確認してください。Google公式・Google公認の商品ではありません。


あなたも記事の投稿・販売を
始めてみませんか?

Tipsなら簡単に記事を販売できます!
登録無料で始められます!

Tipsなら、無料ですぐに記事の販売をはじめることができます Tipsの詳細はこちら
 

この記事の販売者

ツールおじ|GoogleアカウントをAIエージェント化

おう、Gasおじだ。 AIツールとSaaSに散々突っ込んで遠回りした結果、最後に強かったのは「今あるGoogleアカウント」だった。 今ではAIエージェントであらゆるタスクを自動化して無双してるぞ。 GAS、スプレッドシート、フォーム、Gmail、AIエージェントを組み合わせれば、個人でも小さな会社でも、手作業だらけの業務はかなり自動化できる。 ここでは机上のAI論じゃなく、現場で動くテンプレ、コード、手順だけを置いていくぞ。 高いツールを増やす前に、まずお前さんのGoogleアカウントを仕事するエージェントに変えようじゃないか。

この販売者が書いた他の記事

  • Gmailの予約送信ミスを送信前に発見|GASで宛先・日時をdry-run監査

  • Gmailの定型連絡を「送信せず」まとめて下書き化する|Gmail下書きテンプレGAS Pro

    ¥2,980
    1 %獲得
    (29 円相当)
  • スプレッドシートの入力ミスを公開前に発見|GASでルール表をdry-run監査

関連のおすすめ記事

  • Gmailの定型連絡を「送信せず」まとめて下書き化する|Gmail下書きテンプレGAS Pro

    ¥2,980
    1 %獲得
    (29 円相当)
    ツールおじ|GoogleアカウントをAIエージェント化

    ツールおじ|GoogleアカウントをAIエージェント化

  • USBメモリで持ち運べるLinux Mint 22.1と永続的な書き込み領域の作成法【Rufus だけで作成する方法を初心者向けに徹底解説】

    ¥500
    1 %獲得
    (5 円相当)
    SmartSenior

    SmartSenior

  • 毎朝のToDoを区分・優先度・期限つきでまとめる Morning ToDo Mail Pro

    ¥2,980
    1 %獲得
    (29 円相当)
    ツールおじ|GoogleアカウントをAIエージェント化

    ツールおじ|GoogleアカウントをAIエージェント化