プログラムをプログラムで組み立てるときの注意点とは 〜パラメータ化クエリ, SQLインジェクション, エスケープ〜
プログラム自学案内の35回目です。今回は、パラメータ化クエリと、エスケープの考え方を案内します。前回までの記事はこちら。
パラメータ化クエリ
今回の記事は、前回チラ見せしたコードのうち、ここについての話をします。
models/family.js
const result = await client.query(
'DELETE FROM isono_family WHERE full_name=$1 AND age=$2',
[name, age]
);
ここで full_name=$1 とか age=$2 とか書かれているSQL文は、 パラメータ化クエリ (Parameterized query) と呼ばれます。SQLの中の値を、プログラムで、いろいろ変えて実行したいときに必須で、定石の方法です。 node-postgresの取説 の "Parameterized query" の項で紹介されています。
とりあえずは、「SQLの条件句などに色んな値を入れて動かしたいときには、パラメータ化クエリを使いましょう」、とだけ覚えておけばいいのですが、では、なぜこれが必須で定石なのでしょうか?
逆に、一般的に禁じ手とされる方法もあります。パラメータ化クエリを使わずに、SQLを組み立てる方法です。
const result = await client.query(
"DELETE FROM isono_family WHERE full_name=' " + name + "' AND age=" + age
);
もしくは
const result = await client.query(
`DELETE FROM isono_family WHERE full_name='${name}' AND age=${age}`
);
という感じです。
こっちの方がコードとしてシンプルだし、同じように動くはずなのに、なぜ禁じ手なのでしょうか?
禁じ手が禁じ手である理由は、何と3つもあります。
じつは正しくSQL文を組み立てられない、 機能的欠陥 がある
正しくSQL文を組み立てられないことによる、 脆弱性 がある
つど異なるSQLが発行されることにより、性能的に不利 になる
ことに2つ目の脆弱性はマジでヤバいんですね。どれくらいヤバいかの説明は、この記事では割愛しますが、かわりに、SQLインジェクション というキーワードで、ぜひそのヤバさを調べてみてください。
ところで、2度も出てきた「正しくSQL文を組み立てられない」とはどういうことなのでしょうか。このことは、SQLの学習としてではなく、あらゆる言語が持つ、 エスケープ という考え方の学習のなかで、理解したほうが良いでしょう。この記事では、ゆっくりめに説明します。
エスケープの話
あらゆる言語とは?
さきにあらゆる言語はエスケープの考え方を持つ、と言いましたが、あらゆる言語とはどういうことでしょうか? ここでは、コンピュータに指示を与える記法のすべて、ととらえてください。この連載では既に次の7つの記法を紹介していますが、これらには、すべてエスケープの仕組みが備わっています。
HTML
CSS
JavaScript
シェル言語
HTTP
mustacheテンプレート言語 ※
SQL
※ 正確には、mustacheテンプレート言語には狭義のエスケープ構文はありません。ですが、代わりに Set Delimiter という仕組みが備わっています。
ほとんどの言語は、別の言語で書かれたメッセージを扱う
上述の7言語はすべて、文字列を扱おうとします。そして、扱われようとしている文字列の内容は、「その言語以外の言語でかかれたメッセージ」です。
自然言語(日本語や英語など)で書かれた、人間向けのメッセージ
コンピュータ向けの記法(HTMLやSQLなど)で書かれた、コンピュータ向けのメッセージ
ややこしいので、例を見てみましょう。
const result = await client.query(
"DELETE FROM isono_family WHERE full_name='磯野波平' AND age=54"
);
これは全体としてJavaScript言語で書かれていますが、2行目の部分の 文字列 の内容は、JavaScriptではなくSQL言語の文です。そして、SQL文の中にも 文字列 があり、その内容である「磯野波平」は日本語です。ややこしいと思いませんか?
もう一つ例を見てみます。
<a href="https://www.google.com/?gl=us&hl=en">
Google検索 英語版
</a>
これは全体としてHTML言語で書かれていますが、「"」 で囲まれたgoogleのアドレスの部分の 文字列 は HTTPで用いられるURL、「Google検索 英語版」の部分の 文字列 は日本語ですね。
文字列の終わりをハミ出る問題
いままで見てきた例で分かると思いますが、文字列の開始、終了はだいたい、「"」「'」「>」「<」なんていう文字で区切られることが多いです。ですが、この決まりだけだと、文字列の中でこれらの文字を扱いたいときに困ります。
たとえば、元NBA選手のシャキール・オニールが磯野家にやってきたとしますね。
INSERT INTO isono_family VALUES ('Shaquille O'Neal', 51);
このSQLは上手くいきません。英語であるはずの「Shaquille O'Neal」に「'」が含まれるせいで、文字列の枠を「'Neal」が越境し、本来英語としての意味のみを持たせたいはずの「'Neal」が、SQL文としての意味を持ってしまうからです。
では、どうすれば良いでしょうか?
エスケープとは
PostgreSQLでは、「''」(シングルクォート2つ)と書くことで、文字列の終端であるというSQL上の意味が取り除かれた、文字列のなかの「'」文字を表現することができます。
INSERT INTO isono_family VALUES ('Shaquille O''Neal', 51);
このことを、「「'」を エスケープ(escape) する」と言います。英語で escape は 逃亡、脱出という意味です。なぜ逃亡? 「文字列の終端」という意味付けから逃れさせるというニュアンスで、エスケープというようです。
じつは、エスケープはこの連載の「はじめてのJavaScript」でも行われています。
console.log('Hello, World. m(__)m I\'m sorry to be born.');
「コンピュータに対する指示」に、文字列を埋め込むときには、必ずエスケープ処理を!
さきほど述べた通り、プログラムが扱う文字列は大きく、次の二つに大別できます。
人間に向けたメッセージ(日本語、英語、中国語、、、)
コンピュータに対する指示(SQL、JavaScript、HTML、JSON、正規表現、、、)
プログラミングでは、前者と後者双方の文字列を扱いますが、後者、コンピュータに対する指示の文字列をプログラムで組み立てるときには、必ず エスケープを考えなければいけません。ヤッカイですね。
この記事の最初の方に、禁じ手として示したコードを再掲します。
const result = await client.query(
"DELETE FROM isono_family WHERE full_name=' " + name + "' AND age=" + age
);
このコードでは、コンピュータに対する指示であるSQL文字列を組み立てているのに、エスケープ処理がなされていません。このようなコードは、かならず、機能的な欠陥があるコードとみられるのです。具体的には、「変数のnameが "Shaquille O'Neal" を指すとき、SQL文が実行できない」という欠陥を持つのですね。
この欠陥を回避するには、「nameの中の文字列に ' が入っていたら、全部 '' に置き換える 」というようなひと手間(エスケープ処理)を加えなければいけません。面倒ですが、SQL文をプログラムで組み立てようとする限り、この面倒さを避けるわけにはいかないのです。
一方、パラメータ化クエリは、このような欠陥を回避するために、「SQL文字列の組み立て」そのものを回避する方法だったというわけなのでした。
他のエスケープの例を考えてみましょう
これまでの連載では、mustacheテンプレートエンジン をつかって、HTML言語で書かれた画面に、日本語を埋め込んでいました。たとえば、下記のコードでは {{.}} の部分に日本語が埋め込まれます。すなわち、mustacheエンジンによって、HTML言語の文字列が組み立てられます。
list_view.html(抜粋)
あなたが いままでに おだてた人たち
<ul>
{{#history}}
<li>{{.}}さん</li>
{{/history}}
</ul>
では、HTML言語で文字列の終端を示す文字が、{{.}} さんの部分に入ってきたらどうなるでしょうか?
つぎのことを試してみましょう。
おだてる人の名前に、HTMLタグを埋めてみる(例:「滝沢</li>カレン<li>チャン」)
表示された画面で、ブラウザの「ページのソースを表示する」コマンドを実行する
手元に mustacheテンプレートエンジン を使ったアプリが無い方は、mustacheテンプレートエンジンのデモ画面 で、nameに「滝沢</li>カレン<li>チャン」と入れるとどうなるか確かめてみましょう。
{
"header": "Colors",
"items": [
{"name": "滝沢</li>カレン<li>チャン", "first": true, "url": "#Red"},
{"name": "green", "link": true, "url": "#Green"},
{"name": "blue", "link": true, "url": "#Blue"}
],
"empty": false
}
その結果がなぜ起きるのかの理解は、読者の課題とします。納得いくまで、周りの人や google、ChatGPTなんかに訊いてみてください。
まとめと次回予告
今回の記事では、「エスケープ」について紹介しました。小ネタのようで、じつは意外と大切な大ネタだと、思っております。何せ、ややこしい話です。
次回予告です。前回例題としてお見せした家族の追加・削除プログラムの実装例を紹介する予定です。
余談
エスケープという言葉で思い出すのが「大脱走 (The Great Escape)」という映画です。1963年公開の映画だそうなので、もう60年も前の古典ですが、見たことあります? あれは見どころの多い、面白い映画ですよね。見たことない人にはおススメします。ジュディマリの曲にもそういう名前の曲がありますが、有名なのは映画のほうだと思います。
#コラム #プログラミング #JavaScript #SQLインジェクション #エスケープ