はじめに
こんにちは、kodaiと申します。
こちらは脆弱エンジニアの Advent Calendar 2025の17日目の記事になります。
先日、MBSDのセキュリティ勉強会~公開LT会~にオンラインで参加しました。
発表者の方の内容の中で、「日付・時刻にまつわるWebの脆弱性」というテーマで話されている内容がありました。
このLTを聞いていて凄く面白いなと思ったので、LTで話されていた内容+自分が調べて見たことについて、まとめさせていただきます。
Webに関する日付・時刻の位置づけについて
本題のSQLの型変換に入る前に、webにおける日付・時刻の扱われ方についてまとめました。
- webセキュリティにおいて、数字や識別子について注目されることが多く、認可・有効期限・整合性に直結する
- 日付・時刻の型に着目すると、数値・文字列以外にも日付・タイムスタンプなどがある
- フロントエンドでは文字列、APIではJSON文字列、バックエンドでは
DateTime、DBではDATE/DATETIME/TIMESTAMPといったように様々な実装・解釈のされ方があるので、サーバ側の実装を間違えてしまうと脆弱性に繋がる恐れがある
コンピュータサイエンスにおける時刻・日付
-
Unix Time :
1970/1/1 00:00:00 UTCからの経過時間を数値で表現 -
ISO 8601 : 文字列による表現
YYYY-MM-DDThh:mm:ss[.SSS](Z|±hh:mm) - データベース型 :
DATE,DATETIME,TIMESTAMP- DBMSによって意味が違ってくる(例 タイムゾーン変換有無、保存形式、範囲、丸め)
- アプリケーション型 :
Date,DateTime,LocalDateTime(言語による)
webアプリケーションで考えた場合、データベースとの連携はほぼ必須事項になるため、DATE型を意識した実装を行っていない場合は、各レイヤーでの表現方法や解釈ルールの違いから、予期しない脆弱性を生むことに繋がります。
SQLの型によって生まれる解釈の違いについて
webアプリケーションにおいて、型変換が原因でBlind SQL Injectionが発生することがあります。
| DB | 暗黙の型変換 | Version |
|---|---|---|
| PostgreSQL | 型厳密(暗黙キャストはある) | 8.3以降 |
| MySQL | 暗黙の型変換 | 最新バージョンまで |
| MariaDB | 暗黙の型変換 | 最新バージョンまで |
| Oracle | 暗黙の型変換 | - |
| DB2 | 型の組み合わせによって変化 | - |
| MSSQL | 型の組み合わせによって変化 | - |
暗黙の型変換とは?
SQLの実行中に異なるデータ型の値が自動的に他の型に変換される現象のことです。
DBMS によって自動的に行われるものもあります。
以下が具体例です。
例 : MySQLの場合
SELECT '123abc' = 123; --('abc'の部分を数値として解釈できないので0とみなす)
SELECT 1+'1'; -> 2 -- オペランドのどちらかが文字列でも、「数値として解釈できるなら数値に変換してから計算する」
例 : DB2の場合、組み合わせによって暗黙の型変換が行われます
| 挙動 | パターン | 何が起きているか |
|---|---|---|
| 起こる | 異なる数値型同士の演算SMALLINT + INTEGER
|
両方とも「より広い方の数値型」に昇格してから計算される。 この例だと SMALLINT が INTEGER に暗黙キャストされ、INTEGER + INTEGER として評価される。 |
| 起こる | 数値列 = '文字列'WHERE user_id = '100'
|
比較演算では「カラム側の型」が優先されることが多い。user_id が INTEGER なら、右辺 '100' は内部的に INTEGER にキャストされ、user_id = 100 として評価される。 |
| 起こる | 文字列連結'ID-' || 123
|
文字列連結演算子 || では「文字列型」が優先される。123 が文字列 '123' に暗黙キャストされ、`'ID-' |
| 起こる | 代入(VARCHAR 列に数値を INSERT)INSERT INTO users(name_vc) VALUES (123)
|
代入(INSERT/UPDATE)の場面では「列の型」に合わせて変換される。name_vc が VARCHAR なら、123 は '123' に暗黙変換されて格納される。 |
| エラー | 数値 + '文字列'SELECT 1 + '1'
|
加算演算では「数値型」が必須。 DB2 では '1' を数値に暗黙キャストせず、「数値 + 文字列」は不正な組み合わせとしてエラーになる。 |
| エラー | 数値列 = 'abc'WHERE user_id = 'abc'
|
比較のために 'abc' を数値に変換しようとするが、数値に解釈できないためエラー。DB2 はここを「0 にして続行」ではなく、比較自体を失敗扱いにしてくれる。 |
| エラー |
BLOB 型と数値の比較 |
バイナリデータと数値の間には妥当な変換規則がないため、暗黙変換は行われずエラーになる。 |
日付型も内部的には「文字列 or 数値からの変換」という点で、これらの暗黙キャスト規則の延長線上にあります。
誤ったサニタイズについて
SQL Injection対策でプレースホルダやORMが用いられることがありますがプレースホルダを用いらず、AND, OR, ||など禁止文字列を入れて対策されている場合があります。
もしこのような禁止文字列がDATE型に適用されている場合、例えばDATETIME関数の中に適用されている場合、オペランドを使わなくてもBlind SQL Injectionを引き起こすことができます。
PostgreSQLの場合
PostgreSQLにはdateをパースする場合、dateというリテラル型とto_dateという関数型の2種類でDate型にパースすることができます。
PostgreSQL における日付入力と to_date の挙動
| 区分 | 入力例 | 結果 | 解釈・理由 |
|---|---|---|---|
| キャスト | DATE '2025-10-23-' |
パース可能 | 日付入力の汎用パーサが 2025-10-23 までを YYYY-MM-DD として読み取り、末尾の余分な - は無視される。結果として 2025-10-23 という DATE 値になる。 |
| キャスト | DATE '2025-8-32' |
エラー(存在しない日付) |
YYYY-MM-DD としては解釈できるが、「8 月 32 日」は存在しない日付なので date/time field value out of range エラーになる。 |
| キャスト | DATE '20251023' |
パース可能 | 数字 8 桁の入力は YYYYMMDD 形式として解釈される。2025-10-23 という DATE 値に変換される。 |
| キャスト | DATE '/2025/10/23' |
パース可能 | 先頭の / をスキップしつつ、2025/10/23 の部分を YYYY/MM/DD として解釈できるため、2025-10-23 に変換される。 |
| キャスト |
DATE '2025//10/23' など |
エラー | 区切り文字の位置が不正で、YYYY/MM/DD としても他の既知フォーマットとしても解釈できないため、パースエラーになる。 |
to_date |
to_date('2025/10/23','YYYY/MM/DD') |
パース可能 | テンプレート YYYY/MM/DD と入力が完全に一致するため、そのまま 2025-10-23 に変換される。 |
to_date |
to_date('2025/8/32','YYYY/MM/DD') |
エラー(存在しない日付) | フォーマット自体は一致するが、「8 月 32 日」は存在しない日付なのでエラーになる(PostgreSQL 10 以降では to_date も日付妥当性をチェックする)。 |
to_date |
to_date('20251023','YYYY/MM/DD') |
エラー | テンプレートは YYYY/MM/DD だが、入力に / が含まれておらず桁数も合わないため、フォーマット不一致としてエラーになる。本来は 'YYYYMMDD' を使うべきケース。 |
to_date |
to_date('/2025/10/23','YYYY/MM/DD') |
エラー | 先頭の / のせいで、テンプレートの YYYY 部分と最初の 4 文字が一致しない。フォーマットが合わないためエラーになる。 |
to_date |
to_date('2025/11/10/23','YYYY/MM/DD') |
パース可能 | テンプレートが消費するのは先頭の 2025/11/10 までで、その時点で日付は 2025-11-10 として確定する。余分な "/23" 部分は無視されるため、パース自体は成功してしまう。 |
PostgreSQLのDATEキャストとto_dateにはこのようにパースの違いがあり、これを使うことで「アプリケーション側がどちらを使って実装しているか」を推定することができます。
DB2 の場合
DB2 でも、文字列から日付を作る方法がいくつかあります。代表的なのが
-
DATE(expression)… 汎用の日付変換・抽出関数 -
TO_DATE(string-expression, format-string)… フォーマット指定付きの変換関数
(中身はTIMESTAMP_FORMATと同じ)
です。
DATE 関数
DATE(expr) は、引数の型に応じていい感じに DATE 型へキャストしてくれる汎用関数です。
- 文字列の場合: 「DB2 が認識している日付文字列表現」なら DATE に変換される
(ISOYYYY-MM-DD、USAMM/DD/YYYY、EURDD.MM.YYYYなど) - タイムスタンプ文字列の場合: いったん TIMESTAMP として解釈され、その日付部分だけが取り出される
- 7 文字の
yyyynnn(年+年内通算日)も特別扱いでサポートされている(例:1989061→ 1989-03-02)
代表的な例を表にすると、次のようになります。
| 区分 | 入力例 | 結果 | 解釈・理由 |
|---|---|---|---|
| DATE | DATE('2025-10-23') |
2025-10-23 |
ISO 形式 (YYYY-MM-DD) の日付文字列として認識され、そのまま DATE 値になる。 |
| DATE | DATE('10/23/2025') |
2025-10-23 |
USA 形式 (MM/DD/YYYY) の日付文字列として認識される(DB のデフォルト日付フォーマットに依存)。 |
| DATE | DATE('1989061') |
1989-03-02 |
長さ 7 の yyyynnn 形式として扱われ、年+通算日から DATE が計算される公式サンプル。 |
| DATE | DATE('2021-12-13-10.20.30') |
2021-12-13 |
これは TIMESTAMP 文字列表現 (YYYY-MM-DD-HH.MM.SS) として解釈され、DATE で日付部分だけが切り出される |
| DATE | DATE('20211213') |
環境次第だがエラーになりやすい | 標準の ISO / USA / EUR / JIS 形式には「区切りなし 8 桁」の日付は含まれないため、そのままでは認識されず SQLSTATE=22007 系のエラーになることが多い(サイト定義 LOC フォーマットをいじっている環境だと例外あり)。 |
ポイントは、どの文字列が有効かは「データベース側のデフォルト日付フォーマットや互換設定に依存する」 ということです。
アプリ側から見ると「同じ SQL を違う環境に持っていったら急に日付文字列が通らなくなった/逆に通ってしまった」という事故が起こり得ます。
TO_DATE 関数
一方 TO_DATE(string-expression, format-string) は、必ずフォーマット文字列を指定する 変換関数です。
DB2 では TIMESTAMP_FORMAT の別名として実装されており、戻り値は TIMESTAMP です(DATE が欲しければさらに DATE() で包む)。
-- 文字列 '2021-12-13' を YYYY-MM-DD とみなして TIMESTAMP に変換し、DATE 部分だけ取り出す
SELECT DATE(TO_DATE('2021-12-13', 'YYYY-MM-DD')) FROM SYSIBM.SYSDUMMY1;
-- 区切りなし 8 桁でも、フォーマットを合わせれば変換可能
SELECT DATE(TO_DATE('20211213', 'YYYYMMDD')) FROM SYSIBM.SYSDUMMY1;
TO_DATEの代表的な挙動を表にすると次のようになります。
| 区分 | 入力例 | 結果 | 解釈・理由 |
|---|---|---|---|
| TO_DATE | DATE(TO_DATE('2021-12-13','YYYY-MM-DD')) |
2021-12-13 |
フォーマット文字列と入力が一致しているので問題なくパースされる。戻り値 TIMESTAMP から DATE だけ取り出している。 |
| TO_DATE | DATE(TO_DATE('20211213','YYYYMMDD')) |
2021-12-13 |
区切りなし 8 桁の文字列も、'YYYYMMDD' を指定すれば正しく日付に変換できる。 |
| TO_DATE | DATE(TO_DATE('13/12/2021','YYYY-MM-DD')) |
エラー | 入力は DD/MM/YYYY だがフォーマットは YYYY-MM-DD なので、先頭 4 文字の 13/1 を「年」と解釈できず、フォーマット不一致でエラーになる。 |
| TO_DATE | DATE(TO_DATE('2021-12-13 10:20:30','YYYY-MM-DD HH24:MI:SS')) |
2021-12-13 |
日時文字列を TIMESTAMP として解釈し、その日付部分だけを DATE で取り出している。 |
このようにDB2にも誰がフォーマットを決めるかという構図でPostgreSQLのように似た構造になっています。
アプリが「日付だから安全」と思っている入力でも、DB側では 文字列 → 日付パース → 比較 が起きており、その途中で余分な文字が黙殺されたり、解釈されたりすることで結果的に論理条件が崩れることによって、Blind SQL Injectionが起こる可能性があります。
まとめ
SQL文を書くときや、CTFの問題を解くときにSQL文の型に注目してみたことはなんとなくあったものの、深く掘れてはいなかったので、LTで聞いた内容をもとに気になったことを調べることができて良かったです。DATETIMEについてあまり意識してこなかったですが、このような挙動の違いがあることを知れて、何かアプリケーションを実装するときにはこういった細かなことにも気を付けていかなければいけないと実感しました。
あとがき
いろいろ調べながら書いたので間違っているところもあると思います。
その場合は指摘していただけるとありがたいです。
参考文献
https://www.postgresql.jp/docs/9.4/functions-formatting.html
https://www.postgresql.org/docs/current/datatype-datetime.html
https://docs.oracle.com/cd/E13189_01/kodo/docs316/sql_errors.html
https://adamj.eu/tech/2020/03/06/sql-implicit-type-conversion/
https://dev.mysql.com/doc/refman/8.4/en/type-conversion.html
https://www.postgresql.org/docs/current/functions-formatting.html
https://www.ibm.com/docs/en/db2-for-zos/13.0.0?topic=types-implicit-cast-from-string-data-numeric-data
https://www.tekizai.net/entry/db2_sql_date
https://www.ibm.com/docs/en/db2-for-zos/13.0.0?topic=functions-date
https://www.ibm.com/docs/en/db2-for-zos/13.0.0?topic=sf-date
https://www.ibm.com/docs/en/db2-for-zos/13.0.0?topic=functions-timestamp-format-timestamp