Webアプリの入力値を保存してから困る前に|GASで文字列をdry-run検査
ツールおじ|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, '&')
.replace(/</g, '<')
.replace(/>/g, '>')
.replace(/"/g, '"')
.replace(/'/g, ''');
}実行手順
- Google Apps Scriptで新しいプロジェクトを作ります。
- 初期のコードを上のコードへ置き換えます。
- `auditWebAppInputFree` を選び、実行します。
- 実行ログで `dryRun`、`valid`、`issueCount`、`issueCodes`、`changedFieldCount` を確認します。
- `sampleInput` の空欄、文字数、`publicScope` を少しずつ変え、想定したエラーになるか確認します。
- 実際のフォームと接続する前に、コピーしたテスト用プロジェクトで境界値を確認します。
初回は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公認の商品ではありません。
