はらへり日記

腹に弾丸

CTF for ビギナーズ Finalに参加してきました(Write up)

CTFはかじったことはあったけどどんな解き方があるかとか全然わからなかったので参加してきた。

2016.seccon.jp

講義のおかげか、運良くぽんぽん問題が解けて3位になれました。

講義も演習も楽しく取り組むことができました。運営の皆様お疲れ様でしたm( )m

Write up

自分が解けて覚えてるものだけ書いておきます。

解けなかったのはファイル持ち帰ったので時間あるときに取り組んでまたWrite up書きたいと思います。

解けた問題はこんな感じ。

f:id:sota1235:20170129233111p:plain

Misc 100 Welcome

解くだけ。サービス問題。

Misc 200 CountUp Game

問題文の案内通り、サーバに接続するとゲームが始まる。

お互い、3つまで数字を言って21を言ったら負けなゲーム。

ただし必ず相手が先攻で始まる。

問題数が多かったらプログラムで自動化しないと無理だったけど10問だったので手動で解いた。

基本的に相手に5, 9, 13, 17を言わせるようにすれば必ず勝てるのでその通りに解いていく。

Web 100 Classical Injection

ユーザ名とパスワードを入れるテキストフォームが表示される。

データベース系と見てシングルクォーテーションを入れるとエラーが表示される。

そこで以下のようなSELECT文が実行されると仮定してみる。

$sql = "SELECT * FROM users WHERE loginid = '${loginid}' AND password = '${password}'";

そこでユーザ名のテキストフォームに' OR 1 = 1;--と打ち込んで後半の条件文を無視するようにインジェクションするとログインができてflagが表示される。

Web 100 May the extensions be with you.

問題忘れてしまいました…誰かWrite up頼むorz

Web 200 もぐもぐ(・~・)

アプリにアクセスすると検索フォームが表示される。

f:id:sota1235:20170129233921p:plain

適当に文字を打って検索すると検索結果がないと言われる。

SQLインジェクションを疑いシングルクォーテーションを入れるとPHPのエラーが表示される。

エラーから読み取るにSQLの実行結果からループで結果を取って表示しようとしてるっぽい。(たしかfetchArray()でエラーがでてた)

とりあえず講義で教わったようにテーブルスキーマを盗めないか試す。

以下の文字列を検索フォームに入れる。

' UNION SELECT null, null, null, null; #

データベースが何かの特定はエラー分からは推測できないのでMySQL, PostgreSQL, SQLiteの可能性を順番に試した結果、MySQLぽかった。

ここでnullの値を変えていくと(確か)4つでページにデータが表示された。

ので次に以下でテーブル一覧を取る。

' UNION SELECT null, table_name, null, null FROM INFORMATION_SCHEMA.COLUMNS; #

これによりsecret_umasugiというテーブルがあることがわかる。

ちなみにtable_nameを2番めにしてる理由は、1番目だと画面に描画されなかったからです。(もしかしたらソースにはあった?)

というわけで以下のテキストで検索してフラグゲット。

' UNION SELECT * FROM secret_umasugi; #

ちなみにたまたまカラム数が一致したので通ったけどもしこれで通らなかったらカラムの名前を調べたりGROUP_CONCAT()あたりで頑張ることになりそう。

Web 300 Easy SQL Injection

ページにアクセスするとLogin ID, Display name, Passwordを入力できる。

また、ログインボタンと登録ボタンが存在する。

とりあえず普通に登録してみるとログインできることがわかる。

問題名が問題名なのでひとまずシングルクオーテーションを入れてみるとDisplay nameのときだけバリデーションエラーのメッセージが微妙に違うことがわかる。

そこでこんなSQLが書かれていると仮定する。

$sql = "INSERT INTO users VALUES ('${loginId}', '${displayName}', '${password}')";

そこでこんな文字列をDisplay nameのフォームに入れて登録を試す。

', 'pass'); --

これで登録が通り、パスワードがパスワードのテキストフォームに入れたものでなく、'pass'になる。(確か)

ちなみにこの時にコメントアウト#にして試せばMySQLでないことがわかるのでSQLitePostgreSQLのどちらかに絞れる。

ここにSQLインジェクションできることはわかったので次にこのDisplay nameにデータベースから抜いた情報を入れられないか試す。

テーブル情報を引き抜くのを講義資料を元にSQLite, PostgreSQL両方試し、結果的にSQLiteだった。

こんな感じ。

'||(SELECT sql FROM SQLITE_MASTER), 'pass'); --

こうしてログインが成功すると最初はDisplay nameに何もでないがもう一回ログインし直すとusersテーブルのスキーマ情報が抜ける。

あとはそれを元にもう一度インジェクションし、ログインし直して情報を抜いてフラグゲット。

ユーザ名をadminにした理由は勘なのでこれが外れたらLIMIT, OFFSET句あたりを使って一行ずつ抜いていくとかになるんですかね。

'||(SELECT password FROM users WHERE loginid='admin'), 'pass'); --

Forensics 100 みつけてみよう

pcapファイルなので中身をWireSharkで開くとHTTP通信を何回かしている。

とはいえ回数多くて全部読むのがめんどくさくてstrings for100.pcap | grep ctf4bしたら1つだけ中身のあるフラグがあった。

Forensics 200 漏洩した情報を特定せよ

この問題が個人的には面白かった。

まずpcapファイルをWiresharkで開き、ざっと見てみる。

特に怪しいものはなかったのでHTTP通信にフィルタするとこんな感じになる。

f:id:sota1235:20170129235815p:plain

通信の流れを見るとクライアントが

  • GET /c2/xor_cipher_key/
  • POST /c2/leak_info/

としてるのがわかる。問題のシナリオを推測すると情報を漏洩させた人は

  • 情報を他社にリークする
  • その際、情報を暗号化して送信した
  • 暗号化の手段はXOR演算

と推測できる。

まずGET /c2/xor_cipher_key/のレスポンスを見るとこんな感じ。

f:id:sota1235:20170130000023p:plain

ffとだけある。これがXOR演算のためのキーだと仮定する。

その後のPOST /c2/leak_info/のリクエストボディを見る。

f:id:sota1235:20170130000111p:plain

���˝��ϊ������̛̛��ύ��̜�����ϑ��というURLエンコードされた値を送信していることがわかる。

先程のシナリオから逆算するとこの値は

  • 情報にffを用いてXOR演算する
  • その値をURLエンコードする

ことによって得られたと仮定できる。

なのでこれを読み解くには逆のことをすればいい。

超適当スクリプトを書いて実行する。

<?php

$data = '���˝��ϊ������̛̛��ύ��̜�����ϑ��';
$bytes = explode('%', $code);

$answer = '';

foreach ($bytes as $b) {
    $num = hexdec($b);
    $const = hexdec('ff');

    $result = $num ^ $const;

    $answer .= '%'.dechex($result);
}

echo $answer;

これで元のデータをURLエンコードした文字列が得られるのであとはこんな感じでコマンドを叩いてフラグゲット!

f:id:sota1235:20170130000601p:plain

なぜかcが抜けてたけど足してSubmitしたら通った。

Binary 100 HiddenFlag

これを終了3分前に焦って解いて、結果的に3位になれた。

3分で解いたという時点でお察しで、こんなコマンド叩いて解きました。。。

strings bin100_1 | grep ctf4b

stringsコマンド便利すぎた。

参加してみて

CTF4Bはすごく前から参加してみたくて、今回予定も合って参加できてよかった。

短い時間の中で講義も丁寧にやってもらって資料も充実していて、演習ではそれが生かせる問題ばかりだった。

とはいえまだまだビギナーの身なので大会か何かあればちょろっと参加してみたい気持ちです。

参加した皆様、お疲れ様でした!

macOS環境下で複数ファイルの先頭に一括で文字を挿入する

やりたかったこと

既存コードにflowを導入したかったのでとりあえず全JSの先頭行に// @flow weakを挿入したかった。

shell

find ./app/assets/js -type f -name '*.js' -exec sed -i "" -e $'1s/^/\\\/\\\/ @flow weak\\\n/' {} \;

詳しい解説はshellで疲弊したので省略します。 とりあえず気をつけなきゃいけないことは

  • macOSsedBSD系なので使い方がCentOSとかのものと全然違う
  • xargsでpipeするとなぜかうまく動かない

という点です。 xargsがおかしくてもう諦めかけたところでfindコマンドの-execオプションを知って解決した次第です。最高。

まとめ

もうしばらくshellは書きたくないね。

2016年振り返りと2017年目標

いまさらに。

2016年やったこと

発表しまくった

2016年の目標としてアウトプットの量を増やすということを掲げていた。

なので「申し込んでからネタ考える」勢いでLTやカンファレンスに申し込みしまくった。

社内勉強会と外部発表合わせて18枚のスライドを作った計算になる。

失敗した物もあれば成功したものもあって、何にせよアウトプットすることは大事という一言に尽きる。

ただ、常に量を意識してきたので今年は質を意識してアウトプットのレベルを高めていきたい。

http://www.slideshare.net/sotasugiura/presentations

https://speakerdeck.com/sota1235

YAP(achimon)Cに登壇した

最低1回はやろうと思っていたLT以外での外部発表をYAP(achimon)Cで実現できた。

speakerdeck.com

初めての30分のトークで慣れない部分もあったりしたがたくさんの人に聞いてもらったし、PHPカンファレンスとかPHPの勉強会で「DIコンテナの人ですよね!」と話しかけられることがちょいちょいあったので本当に発表してよかった。

PHPカンファレンスでLTした

24歳の目標がPHPカンファレンス登壇と美味しいカルボナーラを作るってことだったのでとりあえず達成できた。

speakerdeck.com

発表機会の中で一番聴衆が多くて緊張したがなんとかこなせてよかった。

来年こそはLTでなくトークを…!絶対に…!

ネットワークスペシャリスト取った

取った。

大学の頃にろくに勉強してなかったことやそもそも学部が情報系ではなかったので基礎的な知識を薄く広く身につけるために取った。

ネットワークに関しては「スリーウェイハンドシェイクしてるんだよね!知ってる!」レベルだったので勉強できて本当によかった。

IPA系の資格は別に取らなくてもいいと言う人が結構多い印象なんだけど、その分野に関しての知識不足を感じてる人には個人的にはおすすめしたい。

ネットワークスペシャリストに関して言えば自分が立てたサーバとPCとがどんな風にパケットをやり取りしてるのかに想いを馳せられるようになるし、知的好奇心をくすぐられるものが多かった。

3年も経てば覚えたことの半分以上は忘れるんだろうけど、まぁよく言われる根っこの知識は定着したと思うのでずっと役に立つものだと思ってる。

転職した

した。

sota1235.hatenablog.com

1月付で別のところで働きます。

ポエムは退職エントリに書いたので別に言うことはないですが、この機会によりいっそう気を引き締めて修行していきたい気持ちです。

2017年の目標

今年何卒

よろしくお願いします

来客(カノジョ)用にVLANを切る話

この記事は

カノジョできないエンジニア Advent Calendar最終日の記事です。ニゲキレナカッタ…

qiita.com

前半ポエムなので技術の話が見たい方は「今日のお話」から読んでどうぞ。

Advent Calendarの参加者の方々、お疲れ様でした

25日間の戦いがようやく終わりを告げようとしています。

チャーハンを作った人、LEDをカノジョとした人、見事カノジョを捕まえ逃走した人。

25日間、涙を禁じ得ない日々を本当にお疲れ様でした。

ちなみに私の死の三日間は

  • 23日 掃除とネットワーク組み
  • 24日 もくもく会
  • 25日 魚を食べる

という大変充実した3日間でした!チクショウ!

このAdvent Calendarに参加する意義

すごく軽い気持ちで参加したカレンダーでしたが、はてブで微妙にバズったり、全然予期しない方向から「最終日wwwwお前かwww頑www張wwれww」みたいな熱いエールを受け取ったりしました。目頭が熱くなっちゃう応援ですね。

そんな感じで少しプレッシャーを感じながら今日まで過ごしてきたわけですが、このアドベントカレンダーに参加する意義をぼんやり考えてました。

ずばり、このカレンダーは傷を舐め合うために書くのではなく「2017年は頑張るぞ!」という自分たちへのエールとして書いてるのではと思っています。

Advent Calendarの記事を見返してみてください。

絶望や嘆きを全面に押し出してる記事は1つもなく、むしろ前向きに技術のネタを更新していくエンジニアたちの記事ばかりだと思います。

これは「俺らはどうせカノジョなんてできないから…」ではなく、「今年はできなかったけど、来年は頑張ろう!」という気持ちの現れなのです。そうに違いありません。異議は認めない。

なので私もそんな心持ちで、どこかにいるであろう待ち人のための技術ネタを書きます。

来年はこのAdvent Calendarの参加者が被らないことを祈りましょう。

今日のお話

ちょっとばかし熱くなってしまいました。ここから本題です。

人肌恋しいクリスマスイブを過ごした私ですが、サンタさんは平等です。

25日起きたら私の枕元にもプレゼントが届いていました。

gyazo.com

カノジョかな?圧縮型かな?と思いワクワクしながら開くとこんなものが。

gyazo.com

1mのLANケーブルが2本、10mのLANケーブルが1本入ってました。

残念ながらカノジョではありませんでしたが、これはもう「ネットワークを組め」というサンタさんからのお告げですね。

ちょうどネットワークスペシャリストに合格してたので私としてもタイムリーなプレゼントです。

せっかくなのでこのLANと先日購入したEdgeRouter Xを使って家で来客用、もといカノジョのためのVLANを組んでみようと思います。

blog.hotolab.net

VLANとは

VLANとはVirtual LANの略です。

本来LAN、つまりLocal Area NetworkはOSI基本参照モデルで言うところのネットワーク層配下を指します。

そしてこのネットワーク層をつかさどるのがルータやL3スイッチです。

gyazo.com

イメージ的にはLANそれぞれの入り口にルータがあるという感じですね。

なのでLANを構築したい場合はルータを足していけばよいのですが、それだと困るケースが存在します。

たとえば、同じ会社で営業部と開発部のPCからつなぐルータを共有したい時があったとしましょう。

その際、安直に両方の部のPCをルータとつないでしまうと、開発部のPCとルータがやり取りしているフレームを営業部がキャプチャ可能な可能性があります。

そうなると業務の内容や情報保護の観点から開発部と営業部のLANは切りたいところですが、そういう場面が出る度にルータを足してネットワーク構成しなおすのは面倒ですし、物理的な制約が出てきた時につらいものがあります。

そんなときに役に立つのがVLANです。

VLANによるブロードキャストドメインの分割

ブロードキャストドメインとは、ブロードキャストパケットを転送する範囲のことです。

ブロードキャストドメインとは|broadcast domain − 意味 / 定義 / 解説 / 説明 : IT用語辞典

このドメインは本来、ルータによって区切られますがVLAN機能を持つルータであれば同じルータにつながったPCを仮想的にブロードキャストドメインで区切ることができます。

VLANの切り方には大きく2つあります。ざっくり説明します。

ポートVLAN

つなぐルートのポートによってVLANを分けます。

設定者は決められたポートにLANケーブルをぶっ刺すだけなので設定は簡単ですが、後でネットワーク構成を変えたい時に若干面倒というデメリットがあります。

タグVLAN

パケットに含まれるMACフレームにVLANを認識するタグを付け足すことで仮想的にVLANを切ります。

ルータは受け取ったパケットのVLANタグを見て、転送すべき端末を判断することでブロードキャストドメインの分割を実現します。

http://www.infraexpert.com/network/vlanz3.gif

出典: http://www.infraexpert.com/study/vlanz1.html

このタグVLANはIEEE 802.1Qで標準化されています。

IEEE 802.1Q - Wikipedia

メリットとしては物理構成にとらわれない柔軟な設定が可能ですが、設定が若干面倒なのとこのフレームの伝送路の機器全てがIEEE 802.1Qに対応している必要があります。

今回使用する方法

今回はEdgeRouter Xを使用するので、ポートVLANを使用して設定します。

設定方法

EdgeRouter Xの初期設定が終わっている前提で話を進めます。

初期設定は以下を参考にするとよいです。

yabe.jp

設定完了後のイメージは以下の通り。

gyazo.com

大人の事情でWANから直接EdgeRouter Xをつなぐことができないので、1つルータを挟んで構築しています。

VLANを設定する

まずはルータの設定画面に入ります。初期設定でルータに固定IPを振っていると思うのでそのIPにアクセスしましょう。

ログインするとDashboardがあるのでまずは下の方のタブからVLANを選択します。

gyazo.com

Add Interfaceをクリックし、VLANを選択しましょう。

今回はeth3, eth4にそれぞれ違うVLAN IDを振ります。

振り終わって設定を見るとこんな感じになってると思います。

gyazo.com

超簡単!

VLANになってるか確かめる

本当にすいません、この作業がまだ完了していなくて26日にまたぎそうなので後ほど追記します…

こういうところがカノジョできない原因ですよね…修行します…

めでたくVLAN設定できてたら

これでカノジョが来た時に「このLANつないでね」と言えばプライベートPCから流れるあんなトラフィックやこんなトラフィックがこっそり盗聴されるなんてことはなくなりました。やったぜ!

え?そんな知識のあるカノジョだったらルータ差し替えられて終わりだって?そんなんはカノジョができてから考えればいいですよチクショウ!

まとめ

来年は頑張って進捗作っていこうな。メリークリスマス!よいお年を!

PHPでISO8061に準拠した日付フォーマットをバリデーションする

この記事は

PHPアドベントカレンダー13日目の記事です。大遅刻ですごめんなさい。

qiita.com

正直、なぜか投稿した気になってしまってました…ちゃんとやらなきゃダメですよね気をつけます…。

したいこと

すいません。タイトルちょっと厳密に言うと違います。もっというとPHP関係ないかもしれない。

厳密には「ISO8061に従い、かつ年月日秒とタイムゾーンまで指定されているかどうか」バリデーションする方法です。

例えば以下のような書式はISO8061に準拠、かつ上記条件を満たしています。

2016-09-30T12:00:00+09:00

ですが、以下の書式はISO8061に準拠しながらも分秒数は指定されていません。

2016-09-30

とある実装で、分秒数まで指定された状態のバリデーションをかけたい場面があったのでどう実現するか考えました。

※ 今後、「ISO8061に従い、かつ年月日秒とタイムゾーンまで指定されているかどうか」といちいち言うと長いので便宜上、「ISO8061完全形」と呼称します。

そもそもISO8061とは

ISOとは皆様御存知、国際レベルでの標準化団体です。

それによって定められたISO8061はざっくり言うなら日付の表記方法を定めたものです。

仕様書を探して初めて知ったんですが、ISOの仕様書は有料みたいですね。なので今回はWikipediaの情報を元に進めました。

ISO 8601 - Wikipedia

方法を考える

まず、行わなければいけないバリデーションは2つです。

  • フォーマットがISO8061完全形であるかどうか
  • 存在する日付かどうか(9/31とかじゃないか的な)

この2つを実現するために、ざっくり以下の方法が考えられます。

  • 既存ライブラリで頑張る
  • 自分で正規表現とか頑張る
  • (あるなら)PHP標準関数とかで頑張る

順番に考えます。

既存ライブラリで頑張る

PHPの日付系ライブラリで真っ先に思いつくのはCarbonです。

carbon.nesbot.com

柔軟なパーサーや豊富でシンプルなAPIやテスタビリティの高さから使ってる人は非常に多いのではないでしょうか。

Carbonを知らないよって方はこの紹介記事がおすすめです。

blog.asial.co.jp

ただ、Carbonにはバリデーションの仕組みはほぼなく、文字列がISO8061完全形かどうかのチェックはできなそうです。

(あったら教えてください)

例えばCarbonは文字列を渡すことでインスタンスを生成できますが、その解釈が柔軟すぎてそれをテキストフォーマットのバリデーションとして使用するのは無理そうです。

例えば2016 12/25という文字列を渡してみるときちんと2016-12-25 20:16:00といった感じでデータが作成されます。

<?php

require __DIR__.'/vendor/autoload.php';

use Carbon\Carbon;

$date = new Carbon('2016 12/25');
echo (string) $date;

では、存在しない日付をチェックする手段としてはどうでしょう?

例えば2016-09-31T12:00:00+09:00はISO8061完全形としては正しいですが、9/31という日付は存在しません。

そこで、ISO8061完全形としてのバリデーションが通った文字列が日付的に有効かどうか調べる方法としてCarbonインスタンスの生成がうまくいくかどうか試してみます。

ちなみにでたらめな文字列を投げると例外を投げてくれるのでおかしな日付もきっと投げてくれるはず。

<?php

require __DIR__.'/vendor/autoload.php';

use Carbon\Carbon;

// Exception
$invalidDate = new Carbon('でたらめ');

では試しにこんなコードを実行してみます。

<?php

require __DIR__.'/vendor/autoload.php';

use Carbon\Carbon;

$date = new Carbon('2016-09-31T12:00:00+09:00');
echo (string) $date;

するとこうなります。

gyazo.com

なぜなのか\(^o^)/

CarbonはDateTimeクラスを継承してる

DateTimeクラスとはPHPの標準クラスです。

PHP: DateTime - Manual

CarbonはこのDateTimeクラスを継承しているので、インスタンス生成に渡される文字列の解釈はDateTimeの仕様に依存しています。

そして、DateTimeではなぜか9/31が通るようになっています。

そしてもっというと9/32は例外を投げます。

<?php

$date1 = new \DateTime('2016-09-31'); // OK
$date2 = new \DateTime('2016-09-32'); // Throws Exception

中のコードまでは読んでないので推測ですが、おおかた正規表現(0[1-9]|[12][0-9]|3[01])みたいなチェックをしているだけな気がします。

ということでCarbonはISO8061完全形のバリデーション、及び存在する日付かどうかのバリデーションには使えなさそうです。

すごく念のためですがCarbon sageなわけではないです。むしろCarbon無いと生きていけない

自分で正規表現とか頑張る

一番避けたいと思いつつ、結論から言うと今回はISO8061完全形フォーマットになっているかどうかのバリデーションはこれで実装しました。

というのも、

  • ISO8061形に対してバリデーションしてるよさげなcomposerライブラリが見つけられなかった
  • PHP標準関数にもISO8061完全形かどうかチェックしてくれるものはなさそうだった

という理由からです。

そもそもISO8061は特定のフォーマットでなく、いくつかのフォーマットがあるのでそれに対してバリデーションというと今回求めてる形式以外も許容しなければなりません。

仕様書に目を通していない以上、適当なことを事実として断言はできませんが世の認識的にも様々な表現方法がある、といった認識が一般的な気がします。

ISO 8601 - Wikipedia

というわけで正規表現ドーン!

<?php

$matches = [];

$isValidFormat = preg_match(
    '/^(\d{1,4})-(0[1-9]|1[012])-(0[1-9]|[12][0-9]|3[01])T([0-1][0-9]|2[0-4]):[0-5][0-9]:[0-5][0-9](Z|(\+|-)([01][0-9]|2[0-4]):?([0-5][0-9])?)$/',
    $date, $matches
) === 1;

$matchesは後ほど、存在する日付かどうかのチェックに使用するためにいくつか文字列を抜き出しています。

読むの嫌になりますよね。読まなくていいです。

こういうものはコードを舐める用に読んでも絶対にミスする可能性があるのでテストケースでカバーしましょう。

思いつく範囲のテストケースを書き出して、ユニットテストでこの正規表現の質を担保しています。

正常系は省略して、異常系のみ書いてます。(以降も同じく)

<?php

$testCases = [
    // 日付のみ指定のケース
    '2016-09-30',
    // 日付と時刻の間のTが抜けてるケース
    '2016-09-01 12:00:00+0900',
    // タイムゾーンが抜けてるケース
    '2016-09-30T12:00:00',
];

あとは日付が存在するかどうかのチェックをすればよさそうです。

PHP標準関数とかで頑張る

日付が実在するかどうかのチェックですが、PHPにはcheckdate()という標準関数があります。

PHP: checkdate - Manual

先ほど実装した正規表現と合わせてこの関数を使用すれば9/31といった存在しない日付も弾くことができます。

<?php

$matches = [];

$isValidFormat = preg_match(
    '/^(\d{1,4})-(0[1-9]|1[012])-(0[1-9]|[12][0-9]|3[01])T([0-1][0-9]|2[0-4]):[0-5][0-9]:[0-5][0-9](Z|(\+|-)([01][0-9]|2[0-4]):?([0-5][0-9])?)$/',
    $date, $matches
) === 1;

$isValidDate = true;

try {
    $isValidDate = checkdate($matches[2], $matches[3], $matches[1]);
} catch (\Exception $e) {
    $isValidDate = false;
}

return $isValidFormat && $isValidDate;

先ほどの正規表現で抜き出した年、月、日をcheckdate()で判別することで存在しない日付のチェックは通らないようになっています。

もちろん、PHPの標準関数を使ってるからと油断せずにテストケースも追加しておきます。

<?php

$testCases = [
    // 日付がでたらめなケース
    '2016-15-01T12:00:00+0900', // 月
    '2016-09-31T12:00:00+0900', // 日
    '2016-09-01T25:00:00+0900', // 時
    '2016-09-01T12:61:00+0900', // 分
    '2016-09-01T12:00:61+0900', // 秒
    '2016-09-31T12:00:00+0900', // 存在しない31日
];

完成形

全部のコードをがっちゃんこするとこんな感じです。

<?php

/**
 * @param string  $date
 * @return bool
 */
function validateDate (string $date) 
{
    $matches = [];
    $isValidFormat = preg_match(
        '/^(\d{1,4})-(0[1-9]|1[012])-(0[1-9]|[12][0-9]|3[01])T([0-1][0-9]|2[0-4]):[0-5][0-9]:[0-5][0-9](Z|(\+|-)([01][0-9]|2[0-4]):?([0-5][0-9])?)$/',
        $date, $matches
    ) === 1;
    $isValidDate = true;

    try {
        $isValidDate = checkdate($matches[2], $matches[3], $matches[1]);
    } catch (\Exception $e) {
        $isValidDate = false;
    }

    return $isValidFormat && $isValidDate;
}

テストケースはこんな感じ。

<?php

$testCases = [
    // 日付のみ指定のケース
    '2016-09-30',
    // 日付と時刻の間のTが抜けてるケース
    '2016-09-01 12:00:00+0900',
    // タイムゾーンが抜けてるケース
    '2016-09-30T12:00:00',
    // 日付がでたらめなケース
    '2016-15-01T12:00:00+0900', // 月
    '2016-09-31T12:00:00+0900', // 日
    '2016-09-01T25:00:00+0900', // 時
    '2016-09-01T12:61:00+0900', // 分
    '2016-09-01T12:00:61+0900', // 秒
    '2016-09-31T12:00:00+0900', // 存在しない31日
];

テストはPHPUnitなりでよしなに🙏

まとめ

PHPの話というよりは文字列をparseする話になってしまいましたが、いろいろと勉強になりました。

ISO8061を知ってるようで知らなかったことやPHP標準関数とかを知れたのでよかったです。

後はテストの大事さですね。こんな正規表現絶対読めない。

とにもかくにも投稿遅れてほんとスイマセン。来年はもっと勉強して「DateTimeクラスの実装直したぜ(ドヤァ」ぐらいの記事書きたいですね。

それでは皆様、メリークリスマス!