Help us understand the problem. What is going on with this article?

0と空文字の比較バグを踏んで大金を騙し取られそうになった話 

More than 1 year has passed since last update.

もう数年前の出来事になるので時効だと思い、この記事をしたためることにしました。
テーマとしては2つ。テストの話と、プログラミングにおける比較の話。
7割が体験談の基づくポエム的内容なので、世の中こんなこともあるんだと鼻ほじる傍ら、
上記2つについてちょっとばかし考える機会にしていただければと思います。

コトのあらまし

※若干の脚色が入ってます

数年前の某日。仲間うちで屋外アクティビティに出かけることになったワイ氏。
施設の予約を取るべく、Webサイトの予約フォームに必要情報を入力していた。

ワイ氏 「よし、これで必要項目入力完了、Enter!」
    (ポチ…)
UI君 「エラーやで」
ワイ氏 「…はい? なんか入力間違えたかなぁ…。
     必須項目は全部入ってるし、特殊文字はないし、メールアドレスの形式間違ってないし、、、」

    (数時間後)

ワイ氏 「…だめや(ゼェゼェ)もう電話で問い合わせよう。」

    (プルプルプル…)
兄ちゃん「もしもしやで」
     担当者のお兄さん(どうやら施設長)が電話の対応をしてくれた。

ワイ氏 「予約フォームがエラーになるのですが、不具合やシステム障害の報告入っていないですか?」
兄ちゃん「そういう報告はないですねぇ。」
ワイ氏 「予約したいですけど、どうすればいいですか?」
兄ちゃん「(どうせ入力ミスやろ)もう一回試してちょ」
ワイ氏 「(何度もやってんねん)分かりました。」

    (数十分後)

兄ちゃん「もしもしでっせ」
ワイ氏 「やっぱり出来んので、電話か何かで予約できませんかね」
兄ちゃん「電話だとエビデンスが残らんのでダメポ」
ワイ氏 「そういうことやったらメールではどうすか?」
兄ちゃん「必要な情報が全てそろってるか確認できないですし」
ワイ氏 (お主が目ですべて確認せいや)
兄ちゃん「フォームからじゃないと利用規約の案内が送られないんですよ」
ワイ氏 (その規約のコピーくらい手元にあるんちゃうんか)

ワイ氏 「じゃぁどうやって予約すればいいですか?」
兄ちゃん「…………」
ワイ氏 (なんでお前が困ってんねん)

ワイ氏 「利用規約ってWebにも乗ってましたよね?内容同じですよね?」
兄ちゃん「載ってますけど…」
ワイ氏 「その規約は隅から隅まで読んでますし、内容にも承諾します。
     入力項目はフォームからコピーするんで不足点があったら連絡ください。
     これで懸念点は解決されたんで、メールで申し込みさせてもらっていいですか?」
兄ちゃん「(めんどくさい客だな)…分かりました」

かくして、メールで内容を送信して予約がなんとか完了した。

<<当日の早朝>>

空の様子「。・゚・(ノД`)・゚・。(土砂降り)」
ワイ氏 「マジか、、、こりゃ中止やな、、、施設にキャンセルの電話かけよう」

兄ちゃん「もしもしどすえ」
ワイ氏 「土砂降りだし、残念なんですけど今日キャンセルしてください」
兄ちゃん「できないですよ?キャンセルするなら全額払ってください。1万うん千円」
ワイ氏 「はいいいいい!!?野外アクティビティですよ?何で?こんなに雨降ってんのに?」
兄ちゃん「規約に書いてあったでしょ?災害級の天候じゃないし地面は水没してないしうちは大丈夫。」
ワイ氏 「ちょ、ちょっとまって…」
    (Webサイトで規約を確認)

ワイ氏 「規約に書いてないじゃないですか!」
兄ちゃん「みなさんに送ってる予約フォームからの返信メールには書いてありますよ。
     こっちは不具合ないって言ってるのに、メールで予約したのはそっちでしょ。
     とにかく、キャンセル料払ってくださいね。後で口座番号送りますから。」
ワイ氏 「(駄々をこねる)」
兄ちゃん「駄々をこねてもダメやで」

ふざけやがってえええええええ!!!
俺の大切な大切な1万円だぞおおおおおおおお!!!!!!

こうして、私の1万うん千円をかけた戦いがはじまった。

怒った私の行動

キャンセルにまつわる大切な事柄がWeb上の規約に正しく書いていなかったり、障害時のサービス代替手段が用意されていないなど、他に突っ込むべきところがたくさんあるのは置いておくにしても、向こうはシステムに何の異常もないと言い張っています。
一方、私は入力項目を何十回もチェックをして、そのチェックした内容のまま相手方にメールも送っているので、
絶対的に入力は正しいと信じています。

お金を返してもらうには、相手に非を認めてもらうのが必須。
そこで、このフォームに対してブラックボックステストを仕掛けることにし、
不具合が出た暁にはそのテスト結果と再現手法を相手にたたきつけてやることとします。

さて、そうと決まればまず計画です。
今まで、他のお客様から問題報告がなかったことから、全ユーザーで起きるエラー(プロセスダウン、サーバーの各種エラー)ではないと思われるので、ユーザー側で変化する事柄(環境の差異、入力値の差異)に注目してテストを計画します。

環境の差異

まずは環境の差異です。
Web系のシステムだと、ポピュラーなのがブラウザです。
そのほか、環境差異でチェックするのがOS、デバイス、ネットワークあたりも調べます。

手元にあるPCに以下の4つのブラウザを用意しました
・IE11
・Edge
・FireFox
・Chrome

デバイスも手元に3台用意しました。(当時なのでVer.が古いです)
・Windows7(FUJITSUノートPC)
・Windows8.1(SONYノートPC)
・Android 4.x系(スマートフォン)

自宅のネットワークは複数系持っていないので、携帯回線を使って2系統用意しました。

入力値の差異

続いて入力値の差異です。

一般的な同値分割を取り入れていきます。
入力フォームが15項目程度あるので、
それぞれの項目で許可されそうな値と許可されなさそうな値の入力パターンを上げておきます。

テストの実施と結果

本来のテストは、これらの項目をマトリクス的にかけ合わせてテストを行っていきます。
そして、実際にはコストとの兼ね合いもあり、要求に対する十分な品質が担保できる範囲(又は目的)の中で項目を削っていくことになります。

しかし、今回は「結局お前のとこのシステムが悪いから予約できなかったんじゃないか」と証拠を叩きつけるためのエビデンスを得ることを目的としてテストを行うので、入力値や環境同士でのかけ合わせはいったんなしにして、うまくいく 又は うまくいかない状態から1項目ずつ変えてテストをしていきます。
これで不具合が出ればコストは比較的安く特定できることになります。

まず環境周りのテストです。
入力値は全てのケースでエラーが出たものと同一を使い
・Win7でブラウザ別テスト、
・FireFox(モバイル含む)でデバイス別テスト
・AndroidでWiFi、PCでテザリングを使ったテスト
を実施していきます。

予約時に少しは試したので、ここで不具合出ることはないだろうと思っていましたが、
ここで出ると出方によっては相手のせいにできないと思い、
内心「でるな、でるな、……」とドキドキでした。
結果は白。全ての環境でエラーになりました。ほかに原因がありそうです。

続いて入力項目のテストです。

各入力項目に対して挙げたパターンのうち、ド正常系の値を1つずつピックアップしました。
名前系であれば漢字2文字。カタカナ3文字。入力必須項目以外もすべて値を入力。数値は全部10で統一。など。
入力後、確認ボタンを押すと、今までエラーになっていたところを通り過ぎて確認画面が見えるではないですか。
この段階で入力項目に対する不具合の可能性がぐっと高くなりました。

ここから考えられる比較的単純な異常系を少し試してみます。
まず必須項目を抜いてみます。すると、項目未入力時用のバリデーションエラーメッセージがちゃんと出ます。
入力必須でない項目を抜いてみます。こちらは、抜いた後も正常に通っているようです。

ここまでで不具合らしいものは出なかったので、とりあえず当てずっぽうはやめて、1項目ずつエラーが出たときの入力内容を移植してみました。
そして、ついにその時が来ます。

昼食の注文人数を入力する項目に0を入力したとたん。
見覚えのあるエラー画面が。
「キタ━━━━(゚∀゚)━━━━!!」
思わず叫んでしまいました。

これで私の勝ち確ですが、ここで喜んではいけません。
あくまで品質テストをした結果を相手に恩着せがましくたたきつけるのですから。

先ほど正常に通った入力値の組合せを基本にし、
そこからテストケースを1項目ずつ試していきます。
結果、最後の項目まで終えてエラーが出たのは上記の1か所だけでした。
良かったのか悪かったのか、、、

ともあれ、上記の結果をまとめて相手方に送りつけました。

テストについて

どうやったら不具合が流出しないテストができたか

テスト工程や技法については、もっと詳しい方がたくさんいますので、最低限の説明にとどめるとして、
どうしたら不具合流出させずに済んだでしょうか。

これは単純な話、いずれかのテスト工程で入力に対する同値分割テストや境界値解析テストを正しくケース作成・実施できていればおそらく防げたものと思えます。
同技法を用いてテストケースを作成すると
数値入力フォームなら、最小-1、最小、最大、最大+1、0、負数、小数、指数表記、空、空白文字、全角数字、文字列、特殊文字、…
などという具合に用意できます。
特にWeb系の入力パターンや特定の機能(時間を扱うもの関連など)ではある程度定石となっているテストケースがあるので、それを活用するのも手です。

また、Webフォームなどは別プロジェクトも含めて頻繁に使うパーツですよね。
こういったモノについては、社内ライブラリを作って共通化をして使いまわしたり、フレームワークを用いると、工数だけではなく品質的にも良いでしょう。

不具合の原因や再現手順の見つけ方

これも人によってやり方が違うかもしれませんが、私は以下のようなことを少なくともしています。
・開発者の場合はエラーメッセージなどが出ているか確認する
・よくあるパターンのエラーを疑ってみる
・処理の成功時と失敗時の違いを洗い出してみる

自分は高校のころからプログラムを組んでおり、デバッグの機会もたくさんありました。
そして、実際に大学のころにこのような経験をしましたが、難なく対応することが出来ました。
社会に出る前にこう言った経験が出来ているのは非常に良かったと思います。

デバッグは経験がものをいうといわれることが多い気がしています。
トラブル解決の手法として体系的にまとめて、文章化できると有用なのかなと思ったりします。
(※良い記事があったら教えてください)

不具合の原因

コードの中身が見れていないので、憶測もかなり含まれますが、
おそらくこのシステムはフロント側にJavaScript、バックエンド側にPHPなどの言語を用いて組まれていたと推測します。

入力完了ボタンを押すたびにバックエンドに送ってチェックしてたのか
はたまたJavaScriptでバリデーションを入れて、バックグラウンドに送っていたのかは不明ですが
おそらく以下の処理のどれかを行っていたのでしょう。

緩やかな比較によるバグ

比較演算子には==と===が用意されている言語があります。PHPやJavaScriptがその一例です。
==は、しばしば「緩やかな比較」と呼ばれ、型の違いを補完して比較をします。(例:123=="123" => true )
一方、===は「厳密な比較」とも呼ばれ、型についても比較を行います。(例:123==="123" => false )
なお、不等演算子にも同様に緩やかな比較(!=や<>)と厳密な比較(!==)があります。

これを知らずに以下のようなバリデーションを実装すると意図しない挙動を起こします。

$x = 0; //entered.
if($x == ''){
    print ("x is not entered.\n");
}else{
    print ("x is entered.\n");
}
// => x is not entered.

NULL判定メソッドの誤った使用

PHPでは変数の中身判定をするためのメソッドがいくつか用意されています。
その中でも代表的なメソッドにis_null()とempty()があります。
is_null()は名前の通り、渡された変数がnullだった場合にtrueを返すメソッドです。
一方empty()は、渡された値が空である場合trueを返すメソッドですが、こちらの「空」の定義がいささか複雑です。
この空にはnullや空文字("")は含まれますが、falseや0、"0"、空配列なども含まれます。

このことを知らずに以下のようなバリデーションを実装すると意図しない挙動を起こします。

$x = ''; //not entered.
if( is_null($x) ){
    print ("x is not entered.\n");
}else{
    print ("x is entered.\n");
}
// => x is entered.

$y = 0; //entered.
if( empty($y) ){
    print ("y is not entered.\n");
}else{
    print ("y is entered.\n");
}
// => y is not entered.

これらの事項はJavaScriptやPHPを専門で触ってきた人や、そういう言語があると分かっている方からすると、常識的な知識に思えるかもしれません。
しかし、複数の言語を横断的に使ってきた技術者が、PHPを使っているプロジェクトにポッと突っ込まれたり、
大学でプログラムを学んできた新卒や、別言語で開発を行ってきた技術者が、Web業界へ就職したりしたときに
比較的よく起こる現象だと私は認識しています。
(かくいう私も、はるか昔にはまったことがあります。)

ぺちぱーやJSerのみなさんも、これからPHPやJavaScriptを触る可能性のあるよろず屋さんや初心者さんも、
この事実を頭の片隅に置いておいてもらえると世の中が少しだけ平和になるのかもしれません。

エピローグ

ワイ氏 「よっしゃ、これで証拠揃ったで。
     ほら見たことか、不具合ちゃんとあるじゃないか。
     報告書書いて送り付けたろ。」

    (数十分後)

ワイ氏 「書けた!でも待てよ、これ本人にだけ送って握りつぶされたら厄介だな…。
     そうや、親会社の社長さん宛てに送ってやろ。」

メール君「拝啓 社長様
     かくかくしかじかで1万うん千円騙し取られそうやで。
     とりあえず不具合の再現できたから、二次被害出ないようにシステム担当者に見せてな。」
    (送信……)

    (数時間後)
ワイ氏 「お、通知きておる。留守電や」
兄ちゃん「ほんっとうに申し訳ありませんでした(土下座)
     お代はいりませんので、勘弁してください。
     あと、次回来た時サービスさせてください。」

ワイ氏 「まぁこういってるし、許してやるか。でもしばらくはココに行きたくないな。」
    (窓の外を見る)
ワイ氏 「もう外真っ暗やん、一日無駄したわ…」

おしまい

まとめ

  • ワイが逆の立場だったらこんな客がいたら嫌だ
  • 比較の問題はプログラミングでよく躓く部分だから気を付けよう
  • 私一人分ぐらいの工数と日当で防げた事案なのでテストはケチらずに行おう
  • 1万数千円は騙し取られずに済んだが、結局自分の労働力を相手に差し出したので自分の日当1日分の損
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away