情報処理安全確保支援士 2011年 春期 午後1 問01
セキュアプログラミングに関する次の記述を読んで、設問1~4に答えよ。
A社は、従業員数100名のソフトウェア開発会社である。A社では、PC向けソフトウェア製品として、“青少年が安全に安心してインターネットを利用できる環境の整備等に関する法律”に基づいた青少年有害情報フィルタリングソフトウェア(以下、Bフィルタという)を開発することとなった。
A社では、Bフィルタの開発にたって、F主任をリーダとした開発チームを編成した。この開発チーム内の体制は、設計を行うDグループ、作成を行うPグループ及び試験を担当する&グループの三つのグループとした。また、プログラム開発に用いる言語は、JIS X 3014“プログラム言語C++”(以下、C++という)とした。
次は、この開発におけるセキュリティの弱性の発見から修正及び再発防止に至る経緯である。
〔URLマッチングの方式設計〕
Dグループでは、Bフィルタの概略機能を図1のようにまとめた。その後、要件定義を経て、URLマッチングによる有害の度合いを決定する方式設計を行い、図2の内容のとおり決定した。


〔詳細設計と作成〕
Dグループでは、図2の方式設計結果に基づいて設計作業を進め、URLをパーセントエンコードする機能を一つの関数として作成することを決定し、その関数の仕様を図3のとおりとした。

その後、Pグループでは、パーセントエンコード関数の作成に取り掛かり、図4に示すソースコードを作成した。


〔脆弱性の発見〕
開発計画に基づき、URL マッチング機能だけがおおよそ完成した時点のものをプロトタイプ1として、Q グループによる試験を実施した。その試験の結果、ある特定の条件下で URL マッチング機能が正常に動作しない不具合が発見された。その結果を受けてプロトタイプ1の動作について詳細に確認したところ、パーセントエンコード関数が実行された直後に、ほかの関数でフラグとして使用している変数(以下、フラグ変数という)が想定外の値に書き換わり、URL マッチング機能が正常に動作しないことが判明した。
ソースコードの調査を行ったところ、パーセントエンコード関数にバッファオーバーフロー脆弱性があり、隣接したメモリ領域に置かれたフラグ変数が想定外の値に書き換えられることが分かった。
〔関数の仕様変更〕
Bフィルタの開発チームは、Bフィルタで同様の脆弱性を作り込まないようにするために、標準C++ライブラリの文字列クラスを使用してパーセントエンコード関数を作り直すとともに、ほかの関数においても作り直しを行った。パーセントエンコード関数の作り直しに当たって、引数及び返却値を文字列型に変更するのに伴い、Dグループでは関数の仕様を図5に示すように変更した。その後、Pグループは図5の仕様に基づいた関数の作成に取り掛かり、図6のソースコードを作成した。
なお、図3の仕様に基づく関数 urlPercentEncode()を呼び出していた部分は、すべて図5の仕様に基づく関数 urlPercentEncodeString()を呼び出すように修正した。


図4のソースコードでは文字列を配列として取り扱っていたので領域の境界をプログラムで管理する必要があったが、図6のソースコードではcので、バッファオーバフローが発生することはなくなった。
〔修正後の確認と再発防止〕
その後、Qグループでは、作り直したプロトタイプ1を試験して、バッファオーバフローが発生しないことを確認した。最後に、F主任は再発防止策として、従来のA社におけるC++開発標準ルールを図7に示すように改訂した。
なお、図7中の下線部は、この改訂によって追加した項目である。

この改訂によって、A社におけるプログラム開発では本件と類似した問題点を作り込むことがなくなり、より安全なソフトウェアを提供することができるようになった。
設問1:
図6中のa、bに入れる適切なメンバ関数名(メソッド名)を解答群の中から選び、記号で答えよ。
解答群
ア:begin
イ:clear
ウ:end
エ:get
オ:*set
力:size
キ:start
ク:tail
模範解答
a:ア
b:ウ
解説
解答の論理構成
- 【問題文】図6では
string::iterator readPoint = src.a();
string::iterator atEOL = src.b();
と記述されており、readPoint と atEOL の 2 つの反復子(イテレータ)で文字列 src 全体を走査しています。 - ループ条件は
while(readPoint != atEOL){ … }
です。反復子が「先頭から末尾(終端位置)まで」を指し示すのが自然でなければ、この条件は成立しません。 - 標準C++ライブラリ string クラスの代表的なメンバ関数は
• begin() : 先頭要素を指すイテレータを返す
• end() : 末尾の次(終端)を指すイテレータを返す
です。その他の候補(clear、size など)はイテレータを返さない、あるいは意味が合致しません。 - したがって
・readPoint は先頭を指す必要があるため a に入るのは “begin”
・atEOL は終端を指す必要があるため b に入るのは “end”
となり、解答は
a:ア
b:ウ
で確定します。
誤りやすいポイント
- size() と混同する
size() は要素数を返す関数でありイテレータ型ではないため、代入自体がコンパイルエラーになります。 - 末尾位置のイメージ違い
end() が「最後の文字」ではなく「最後の次」を指すイテレータである点を忘れると、ループ条件を誤って「<=」などに置き換えてしまい off-by-one エラーを招きます。 - clear() の機能誤認
イテレータを返しそうに見えても、clear() は内容を消去する副作用のみを持つため全く用途が異なります。
FAQ
Q: なぜ const_iterator ではなく iterator を使っているのですか?
A: ループ内で読み出し専用ですが、後続の処理でイテレータを使った可変操作を想定しているケースなどでは iterator を使うことがあります。本問の主眼はバッファオーバーフロー回避であり、イテレータ型の選択は必須要件ではありません。
A: ループ内で読み出し専用ですが、後続の処理でイテレータを使った可変操作を想定しているケースなどでは iterator を使うことがあります。本問の主眼はバッファオーバーフロー回避であり、イテレータ型の選択は必須要件ではありません。
Q: end() が「末尾の次」を返すメリットは?
A: 境界チェックが != end() で統一でき、範囲 [begin(), end()) が半開区間となることで STL のアルゴリズム全体と整合が取れるからです。
A: 境界チェックが != end() で統一でき、範囲 [begin(), end()) が半開区間となることで STL のアルゴリズム全体と整合が取れるからです。
Q: バッファ長の上限(128 バイト)チェックはどこで行うべき?
A: 本問では URL 正規化の前提条件として提示されています。実装ではエンコード後の文字列長が増える場合もあるため、マッチング処理の直前など別途長さ検証を行うのが安全です。
A: 本問では URL 正規化の前提条件として提示されています。実装ではエンコード後の文字列長が増える場合もあるため、マッチング処理の直前など別途長さ検証を行うのが安全です。
関連キーワード: イテレータ、バッファオーバーフロー、文字列クラス、ポインタ操作、範囲チェック
設問2:図4のソースコードで発見された脆弱性について、(1)、(2)に答えよ。
(1)バッファオーバフローが発生するメモリ領域はどこか。図4中の変数名を用いて20字以内で述べよ。
模範解答
dstが指し示す領域
解説
解答の論理構成
-
脆弱性の種類を確認
問題文には「パーセントエンコード関数にバッファオーバーフロー脆弱性があり」と明示されており、対象は「図4」の関数です。 -
書き込み先バッファの特定
「図4」1 行目に
void urlPercentEncode(char *dst, char *src, int n)
とあり、dst が書き込み先バッファであることが分かります。 -
上限チェックの不備
ループ条件は 6 行目
while(src[srcPos] != '\0' && dstPos < n){
で行っていますが、エンコード対象文字の場合は 18〜22 行目で 最大3バイト を一度に dst に書き込みます。dst[dstPos++] = '%'; // 1 バイト ... dst[dstPos++] = x; // 2 バイト目 ... dst[dstPos++] = x; // 3 バイト目しかし dstPos < n の判定は 1回のループ開始時点のみ なので、ループ途中で n を超過してもチェックされず、dst の後続領域に上書きが発生します。 -
被害が出る領域
問題文は「隣接したメモリ領域に置かれたフラグ変数が想定外の値に書き換えられる」と説明しており、これは dst の直後にあった変数への不正書き込みです。
したがって、オーバフローが起きるメモリ領域は「dst が指し示す領域」と結論付けられます。
誤りやすいポイント
- src 側を疑ってしまう
読み出しバッファである src はサイズを超えて読み込まれてもメモリアクセス違反になりにくく、今回の脆弱性の直接原因ではありません。 - n のチェックは十分と誤認
1ループ3バイト書き込む場合があることに気付かないと、条件式 dstPos < n で十分と勘違いしやすいです。 - 「フラグ変数」そのものを回答に書く
設問は「バッファオーバフローが発生するメモリ領域」を聞いており、書き換えられた側ではなく あふれ出した側 を答える必要があります。
FAQ
Q: dstPos + 2 < n と書けば脆弱性は解消しますか?
A: エンコードしない場合は1バイトしか書かないため条件が複雑になります。安全策としては今回のように標準ライブラリ string へ置き換える方が確実です。
A: エンコードしない場合は1バイトしか書かないため条件が複雑になります。安全策としては今回のように標準ライブラリ string へ置き換える方が確実です。
Q: なぜ src 側に長さ制限(128バイト)があるのにオーバフローするのですか?
A: 128 バイトは正規化後の URL 長さです。関数単体ではその制約を知らないため、独立して呼び出されれば長さ無制限となり得ます。
A: 128 バイトは正規化後の URL 長さです。関数単体ではその制約を知らないため、独立して呼び出されれば長さ無制限となり得ます。
Q: 配列ではなく std::vector を使えば安全になりますか?
A: 添字アクセスを使う限り境界チェックを自前で行う必要があり、push_back を使った string/vector の方が安全です。
A: 添字アクセスを使う限り境界チェックを自前で行う必要があり、push_back を使った string/vector の方が安全です。
関連キーワード: バッファオーバフロー、境界チェック、可変長エンコード、ポインタ操作、安全な文字列操作
設問2:図4のソースコードで発見された脆弱性について、(1)、(2)に答えよ。
(2)図4の6行目から始まるwhile文の副文(ループ本体)の実行において、バッファオーバフローが発生するための条件を、図4中の変数名を用いて60字以内で述べよ。
模範解答
dstが指し示す領域の残りが2バイト以下となった時点で、srcにエンコードすべき文字があった場合
解説
解答の論理構成
-
【問題文】の「パーセントエンコード関数にバッファオーバーフロー脆弱性があり」とあるとおり、原因は urlPercentEncode() 内の領域管理にあります。
-
図4‐6 行目の while 条件は
while(src[srcPos] != '\0' && dstPos < n){
で、dstPos < n という“1 バイト以上空いていれば継続”という判定しか行っていません。 -
しかし図4‐18~22 行目ではエンコード対象文字を見つけるとdst[dstPos++] = '%'; // 1 バイト x = (src[srcPos] & 0x00f0) >> 4; dst[dstPos++] = …; // 2 バイト目 x = src[srcPos++] & 0x000f; dst[dstPos++] = …; // 3 バイト目の計 3 バイトを書き込みます。
-
したがって、ループ開始時点で n - dstPos が「1 バイトまたは 2 バイト」しか残っていない状態でエンコード対象文字が現れると、3 バイト目の書込みで dst の領域外に到達し、隣接メモリ(フラグ変数など)を上書きしてバッファオーバフローが発生します。
-
よって模範解答は「dst が指し示す領域の残りが 2 バイト以下となった時点で、src にエンコードすべき文字があった場合」となります。
誤りやすいポイント
- 「ナル終端を付与できないだけ」と思い込み、3 バイト連続書込みによる境界超過を見落とす。
- dstPos < n なので安全だと早合点し、必要バイト数(1 文字で最大 3 バイト)を考慮しない。
- 逆に「残り 1 バイトのときのみ危険」と狭く捉え、2 バイト残りの場合を失念する。
- src 側の読込位置(srcPos)ばかり注視し、dst 側の書込み量を軽視する。
FAQ
Q: そもそも非エンコード文字のときはバッファオーバーフローしないのですか?
A: はい。非エンコード文字は 1 バイトしか書き込まないため、dstPos < n の判定で領域をはみ出すことはありません。
A: はい。非エンコード文字は 1 バイトしか書き込まないため、dstPos < n の判定で領域をはみ出すことはありません。
Q: n は「書込める最大バイト数」と仕様にありますが、ナル終端の 1 バイトを含むのですか?
A: 仕様には「ナル文字で終了することは保証されない」とあるため、n に終端用バイトを含めるか否かは呼び出し側依存です。本脆弱性の本質は 3 バイト連続書込みで境界を超える点にあります。
A: 仕様には「ナル文字で終了することは保証されない」とあるため、n に終端用バイトを含めるか否かは呼び出し側依存です。本脆弱性の本質は 3 バイト連続書込みで境界を超える点にあります。
Q: 早い段階で dst を十分なサイズにしておけば問題は起こらないのでは?
A: その通りですが、入力長を正確に予測できない場合は安全に確保しきれません。書込み前に「必要バイト数」を評価するか、図6 のようにサイズを自動管理する string などを用いるのが推奨されます。
A: その通りですが、入力長を正確に予測できない場合は安全に確保しきれません。書込み前に「必要バイト数」を評価するか、図6 のようにサイズを自動管理する string などを用いるのが推奨されます。
関連キーワード: バッファオーバフロー、パーセントエンコード、配列境界チェック、不正メモリアクセス
設問3:
本文中のcでは、図4で存在したバッファオーバフロー脆弱性が、図6では存在しない技術的理由を述べている。cに入れる適切な字句を60字以内で述べよ。
模範解答
c:文字列をstringクラスとして取り扱っており、領域の境界は処理系が管理することとなる
解説
解答の論理構成
- 【問題文】では、旧実装について「図4のソースコードでは文字列を配列として取り扱っていたので領域の境界をプログラムで管理する必要があった」と明示しています。
- その後の改修方針として「標準C++ライブラリの文字列クラスを使用してパーセントエンコード関数を作り直す」と記述し、図5で関数シグニチャを「string urlPercentEncodeString(string src)」へ変更しています。
- さらに、「図6のソースコードではcので、バッファオーバフローが発生することはなくなった。」と背景を説明しています。
- 文字列クラス string は格納領域の確保・拡張・終端管理を処理系(ライブラリ)が自動で行います。利用側は push_back などのメンバ関数を呼ぶだけで済み、配列添字演算で自力管理する必要がありません。
- したがって、改修後は「領域の境界」を開発者が意識せずに済み、配列境界を越えて書込む不具合——すなわちバッファオーバフロー——が構造的に除去されるため、c には「文字列をstringクラスとして取り扱っており、領域の境界は処理系が管理することとなる」が入ります。
誤りやすいポイント
- 「イテレータを使えば安全」と早合点し、根本理由を“イテレータ”に求めてしまう。安全性を担保しているのは string が内部でサイズ管理している点です。
- 「push_back も配列添字と同じ書込み」と誤解し、push_back でも境界チェックがないと思い込む。string は必要に応じて自動で再確保します。
- 「UTF-8 化や128バイト制限があるから安全」と問題の主旨を取り違える。今回の質問はバッファオーバフロー防止の技術的根拠を問うています。
FAQ
Q: string でも不正入力で例外が出ることはありますか?
A: メモリ不足などで再確保に失敗した場合は std::bad_alloc が送出される可能性はあります。ただし配列境界を越えて隣接メモリを破壊するタイプのバッファオーバフローは発生しません。
A: メモリ不足などで再確保に失敗した場合は std::bad_alloc が送出される可能性はあります。ただし配列境界を越えて隣接メモリを破壊するタイプのバッファオーバフローは発生しません。
Q: vector では同じ効果を得られますか?
A: はい。vector も動的にサイズを管理するコンテナなので、添字アクセスより push_back や at() を用いれば同様に境界越えの書込みを防げます。
A: はい。vector も動的にサイズを管理するコンテナなので、添字アクセスより push_back や at() を用いれば同様に境界越えの書込みを防げます。
Q: 配列を使ったままでも安全に書く方法はありますか?
A: std::array と範囲チェック関数、あるいは安全なラッパー関数を導入すれば防げますが、運用コストとヒューマンエラーを考慮すると string などの高水準コンテナを用いるほうが確実です。
A: std::array と範囲チェック関数、あるいは安全なラッパー関数を導入すれば防げますが、運用コストとヒューマンエラーを考慮すると string などの高水準コンテナを用いるほうが確実です。
関連キーワード: バッファオーバフロー、文字列クラス、領域管理、ポインタ操作
設問4:
図7について、文字列の取扱いで本件と類似した問題点の作り込みを未然に防ぐのに有効となるようにd〜fに入れる適切な字句を、それぞれ8字以内で答えよ。
模範解答
d:文字型の配列
e:上限チェック
f:代入
解説
解答の論理構成
-
原因となった実装の確認
- 図4では char *dst をはじめとする生ポインタと配列添字で文字列を扱っています。典型例が
dst[dstPos++] = src[srcPos++];(図4 14行目)です。 - その結果、パーセントエンコードで1文字が3文字に増える場面などでバッファ境界を越え、フラグ変数まで上書きするバッファオーバフローが発生しました。
- 図4では char *dst をはじめとする生ポインタと配列添字で文字列を扱っています。典型例が
-
再発防止のために盛り込まれたルール
- 図7「文字列処理を行う場合、文字列を格納するための d の使用を禁止する。」
→ 生ポインタや配列ではなく安全なクラス(string など)を使わせる意図なので「文字型の配列」が妥当です。 - 図7「配列又はポインタに添字を指定して書込みを行う場合、e を毎回実施する。」
→ 越境を防ぐためには必ず境界確認が必要ですから「上限チェック」。 - 図7「引数としてポインタを受け取った場合、添字指定での f は禁止する。」
→ ポインタ経由での書込みが事故を誘発するので、添字付きで“読む”のは許容し、“書く”=「代入」を禁止するという文脈です。
- 図7「文字列処理を行う場合、文字列を格納するための d の使用を禁止する。」
-
以上より
- d:文字型の配列
- e:上限チェック
- f:代入
誤りやすいポイント
- 「d」を“C文字列”と書くと配列でなくリテラルを含む概念になり不適切です。
- 「e」を“長さ確認”や“バウンダリチェック”とすると原文のニュアンスとずれ、8字以内要件も満たしにくくなります。
- 「f」を“アクセス”と誤答すると「読み」も含めて全面禁止と解釈され、問題文の意図(書込み禁止)と食い違います。
FAQ
Q: 文字列クラスを使えば完全に安全なのですか?
A: 図6のように string を用いれば自動でサイズ管理されバッファオーバフローは防げますが、論理的な桁あふれや意図しない巨大文字列生成は別途対策が必要です。
A: 図6のように string を用いれば自動でサイズ管理されバッファオーバフローは防げますが、論理的な桁あふれや意図しない巨大文字列生成は別途対策が必要です。
Q: なぜ添字での読み取りは許され、書込み(代入)だけ禁止なのですか?
A: 読み取りは境界外でも即クラッシュせず未定義動作に至りにくい一方、書込みは即メモリ破壊に直結するため、より厳しく制限しています。
A: 読み取りは境界外でも即クラッシュせず未定義動作に至りにくい一方、書込みは即メモリ破壊に直結するため、より厳しく制限しています。
Q: 上限チェックは具体的にどのように実装すれば良いですか?
A: 書込み前に「予定書込み位置+書込みバイト数 ≤ 予約サイズ」を必ず判定し、超える場合は切り詰めるかエラーを返して処理を中断します。
A: 書込み前に「予定書込み位置+書込みバイト数 ≤ 予約サイズ」を必ず判定し、超える場合は切り詰めるかエラーを返して処理を中断します。
関連キーワード: バッファオーバフロー、ポインタ操作、境界検査、標準ライブラリ、エンコーディング


