Google Apps Scriptで、HtmlServiceを使ってWebアプリを作成する場合、GAS側で持っている値をHTML側に受け渡したいことが多々あると思います。
主な受け渡し方法としては、スクリプトレットを使う方法と、google.script.runを使う方法がありますが、この記事では、スクリプトレットを使って受け渡す方法を説明します。
強制出力スクリプトレット<?!= 変数 ?>
を使う場合は、不正な値の混入に注意してください。
記事の最後に説明がありますので、気になるようであればご覧ください。
スクリプトレットでの受け渡しの基本
スクリプトレットを使って、Google Apps Script側の値をHTML側に受け渡す方法をいくつか紹介します。
方法1: グローバル変数を受け渡す
Google Apps Script側(コード.gsなど)でグローバル変数として定義した場合は、HTML側のスクリプトレットで直接参照することができます。
const message = 'Hello'; // グローバル変数
function doGet() {
const template = HtmlService.createTemplateFromFile('index');
return template.evaluate();
}
<script>
console.log(<?= message ?>); // コンソールに Hello が出力される
</script>
方法2: テンプレートにプッシュして受け渡す
Google Apps Script側(コード.gsなど)でHtmlTemplateオブジェクトのプロパティとして値を代入することで、HTML側のスクリプトレットで値を参照することができます。
function doGet() {
const message = 'Hello';
const template = HtmlService.createTemplateFromFile('index');
template.message = message;
return template.evaluate();
}
<script>
console.log(<?= message ?>); // コンソールに Hello が出力される
</script>
方法3: テンプレートから関数を呼び出して戻り値を受け取る
Google Apps Script側(コード.gsなど)で、受け渡したい値を返す関数を定義し、HtmlTemplate側でその関数を呼び出すことで、値を受け取ることができます。
function doGet() {
const template = HtmlService.createTemplateFromFile('index');
return template.evaluate();
}
function getMessage() {
const message = 'Hello';
return message;
}
<script>
console.log(<?= getMessage() ?>); // コンソールに Hello が出力される
</script>
これ以降の解説はシンプルにしたいため、方法2: テンプレートにプッシュして受け渡すパターンを題材として扱います。
Boolean(論理値)を受け渡す
論理値を受け渡す場合のコードサンプルです。
function doGet() {
const template = HtmlService.createTemplateFromFile('index');
template.bool = true;
return template.evaluate();
}
<script>
const bool_A = JSON.parse(<?= bool ?>); // 例A
const bool_B = <?!= bool ?>; // 例B
console.log(bool_A);
console.log(bool_B);
</script>
出力されたHTMLは下記のとおりです。
<script>
const bool_A = JSON.parse('true');
const bool_B = true;
console.log(bool_A);
console.log(bool_B);
</script>
コンソールへの出力は下記のとおりでした。
true
true
Number(数値)を受け渡す
数値を受け渡す場合のコードサンプルです。
function doGet() {
const template = HtmlService.createTemplateFromFile('index');
template.num = 123;
return template.evaluate();
}
<script>
const num_A = Number(<?= num ?>); // 例A
const num_B = JSON.parse(<?= num ?>); // 例B
const num_C = <?!= num ?>; // 例C
console.log(num_A);
console.log(num_B);
console.log(num_C);
</script>
出力されたHTMLは下記のとおりです。
<script>
const num_A = Number('123');
const num_B = JSON.parse('123');
const num_C = 123;
console.log(num_A);
console.log(num_B);
console.log(num_C);
</script>
コンソールへの出力は下記のとおりでした。
123
123
123
String(文字列)を受け渡す
文字列を受け渡す場合のコードサンプルです。
function doGet() {
const template = HtmlService.createTemplateFromFile('index');
template.str = 'abc';
return template.evaluate();
}
<script>
const str = <?= str ?>;
console.log(str);
</script>
出力されたHTMLは下記のとおりです。
<script>
const str = 'abc';
console.log(str);
</script>
コンソールへの出力は下記のとおりでした。
abc
配列を受け渡す
配列を受け渡す場合のコードサンプルです。
function doGet() {
const template = HtmlService.createTemplateFromFile('index');
template.ary = [1, 'a', [2, 'b']];
return template.evaluate();
}
<script>
const ary_A = JSON.parse(<?= JSON.stringify(ary) ?>); // 例A
const ary_B = JSON.parse('<?!= JSON.stringify(ary) ?>'); // 例B
console.log(ary_A);
console.log(ary_B);
</script>
出力されたHTMLは下記のとおりです。
例Aの方法では、ダブルクオートがエスケープされて\x22になっています。
<script>
const ary_A = JSON.parse('[1,\x22a\x22,[2,\x22b\x22]]');
const ary_B = JSON.parse('[1,"a",[2,"b"]]');
console.log(ary_A);
console.log(ary_B);
</script>
コンソールへの出力は下記のとおりでした。例Aの方も問題なくparseできているようです。
0: 1
1: "a"
2: Array(2)
0: 2
1: "b"
0: 1
1: "a"
2: Array(2)
0: 2
1: "b"
オブジェクトを受け渡す
オブジェクトを受け渡す場合のコードサンプルです。
function doGet() {
const template = HtmlService.createTemplateFromFile('index');
template.obj = {
id: 1,
name: 'taro',
props: {
age: 33,
hobby: 'guitar'
}
};
return template.evaluate();
}
<script>
const obj_A = JSON.parse(<?= JSON.stringify(obj) ?>); // 例A
const obj_B = JSON.parse('<?!= JSON.stringify(obj) ?>'); // 例B
console.log(obj_A);
console.log(obj_B);
</script>
出力されたHTMLは下記のとおりです。
例Aの方法では、ダブルクオートがエスケープされて\x22になっています。
<script>
const obj_A = JSON.parse('{\x22id\x22:1,\x22name\x22:\x22taro\x22,\x22props\x22:{\x22age\x22:33,\x22hobby\x22:\x22guitar\x22}}');
const obj_B = JSON.parse('{"id":1,"name":"taro","props":{"age":33,"hobby":"guitar"}}');
console.log(obj_A);
console.log(obj_B);
</script>
コンソールへの出力は下記のとおりでした。例Aの方も問題なくparseできているようです。
{
"id": 1,
"name": "taro",
"props": {
"age": 33,
"hobby": "guitar"
}
}
{
"id": 1,
"name": "taro",
"props": {
"age": 33,
"hobby": "guitar"
}
}
Dateオブジェクト(日付)を受け渡す
Dateオブジェクトを受け渡す場合のコードサンプルです。
function doGet() {
const template = HtmlService.createTemplateFromFile('index');
template.date = new Date();
return template.evaluate();
}
<script>
const date_A = new Date(JSON.parse(<?= JSON.stringify(date) ?>)); // 例A
const date_B = new Date(Number(<?= (date.getTime()) ?>)); // 例B
const date_C = new Date(<?!= date.getTime() ?>); // 例C
console.log(date_A);
console.log(date_A.getTime());
console.log(date_B);
console.log(date_B.getTime());
console.log(date_C);
console.log(date_C.getTime());
</script>
出力されたHTMLは下記のとおりです。
2つ目の方法では、ダブルクオートがエスケープされて\x22になっています。
<script>
const date_A = new Date(JSON.parse('\x222023-09-26T15:02:26.198Z\x22'));
const date_B = new Date(Number('1695740546198'));
const date_C = new Date(1695740546198);
console.log(date_A);
console.log(date_A.getTime());
console.log(date_B);
console.log(date_B.getTime());
console.log(date_C);
console.log(date_C.getTime());
</script>
コンソールへの出力は下記のとおりでした。いずれも同じ結果となります。
Wed Sep 27 2023 00:02:26 GMT+0900 (日本標準時)
1695740546198
Wed Sep 27 2023 00:02:26 GMT+0900 (日本標準時)
1695740546198
Wed Sep 27 2023 00:02:26 GMT+0900 (日本標準時)
1695740546198
クロスサイトスクリプティング(XSS)
上述の例では、出力スクリプトレット<?= 変数 ?>
と、強制出力スクリプトレット<?!= 変数 ?>
両方の例が紹介されていますが、Google公式の解説にもあるとおり、強制出力スクリプトレットは信用できない値(≒ユーザーが入力した値)を扱う際には使わない方が安全です。
なぜ使わない方が良いかを説明するために、強制出力スクリプトレットを悪用したクロスサイトスクリプティング(XSS)攻撃をされるケースを考えてみました。
すみませんが、私の浅い知識では不自然な例しか思い浮かびませんでした・・・。不備やお気づきの点などありましたら、コメントなどで是非ご指摘ください。
サンプルとして、賛同者を募るサイトを作ってみました。
サイトを開くと、今までに賛同した人の一覧が表示されます。
名前と年齢を入力して登録ボタンを押せば、賛同者としてDB(スプレッドシート)に登録されます。
さて、早速、悪意のある値を入力して登録してみます。
"]]'); location.href='https://web-breeze.net'; //
悪意のある値がスプレッドシートに登録されてしまいました。
さて、この状態でサイトに再度アクセスすると・・・
スクリプトが意図しない形に改変されてしまい、別のサイトに飛ばされてしまいました。
この例では私のブログに飛んでいますが、フィッシングサイトなどに飛ばされると、よろしくないですね。
なお、不正な値を埋め込まれてしまったサイトのソースは、下記のような感じです。
JSON.parse()がうまいこと途中で終わらせられて、その後にlocation.href=…が埋め込まれました。
// 元のコード
const supporters = JSON.parse('<?!= JSON.stringify(supporters) ?>');
// 出力されたコード
const supporters = JSON.parse('[["山田 太郎",33],["田中 花子",28],["佐藤 健太",49],["\"]]'); location.href='https://web-breeze.net';
・・・ということで、強制出力スクリプトレットは危険な場合があることがわかりました。
本来は、データを登録するタイミングでエスケープや不正チェックをするはずなので、こんなザルな事にはならないと思います。
・・・が、特段理由がなければ、出力スクリプトレットの方を使うのが良いかと思います。
最後に、今回の脆弱なサンプルサイトのコードを掲載いたします。
ツッコミどころが多い気もしますので、あくまで参考程度ということで・・・。
function doGet() {
const template = HtmlService.createTemplateFromFile('index');
template.supporters = getSupporters();
return template.evaluate();
}
/**
* 賛同者をスプレッドシートに追加する
*/
function registration(name, age) {
SpreadsheetApp
.getActiveSpreadsheet()
.getSheetByName('Supporters')
.appendRow([name, age]);
}
/**
* 賛同者一覧を二次元配列で取得する
*/
function getSupporters() {
return SpreadsheetApp
.getActiveSpreadsheet()
.getSheetByName('Supporters')
.getDataRange()
.getValues().slice(1);
}
<!DOCTYPE html>
<html>
<head>
<base target="_top">
</head>
<body>
<h2>賛同者一覧</h2>
<ul id="list"></ul>
<h2>賛同する</h2>
<label for="name">名前:</label>
<input id="name" type="text">
<label for="age">年齢:</label>
<input id="age" type="number">
<button onclick="registration()">登録</button>
<script>
/**
* スプレッドシートに賛同者(氏名, 年齢)を追加する
*/
function registration() {
const name = document.getElementById('name').value;
const age = document.getElementById('age').value;
google.script.run.withSuccessHandler(() => {
alert('登録しました');
}).registration(name, age);
}
/**
* 賛同者一覧を表示する
*/
function createList() {
// GAS側で作成した配列を受け取る
const supporters = JSON.parse('<?!= JSON.stringify(supporters) ?>');
// 賛同者一覧を作成
const ul = document.getElementById('list');
supporters.forEach((supporter) => {
const li = document.createElement('li');
li.innerHTML = `${supporter[0]}(${supporter[1]})`;
ul.appendChild(li);
})
}
/**
* 初期処理
*/
window.addEventListener('DOMContentLoaded', () => {
createList();
});
</script>
</body>
</html>
コメント