Webシステムや業務アプリケーションを安全に運用するには、ネットワーク機器による防御だけでなく、開発段階で脆弱性を作り込まないセキュアプログラミングの知識が不可欠です。午後試験において、セキュアプログラミングの穴埋め問題は合否を大きく左右する重要な得点源です。ソースコードの文法理解にとどまらず、脆弱性が発生する根本メカニズムを把握し、それを防ぐ処理をコードレベルで記述できる能力が求められます。
本記事では、過去問の出題傾向を徹底分析し、穴埋め問題で頻出するパターンと具体的な解法・回答例を詳しく解説します。
セキュアプログラミングの穴埋め問題とは?過去問から読み解く出題傾向
セキュアプログラミングの穴埋め問題は、不完全または脆弱性を含むソースコードが提示され、それを修正・完成させる形式で出題されます。まずは出題傾向と、問題を解く上でのベースとなる考え方を解説します。
なぜプログラミングの実装力が問われるのか
システムの脆弱性の大部分は、実装段階でのコーディングミスや、セキュリティを考慮しない仕様設計に起因します。WAF(Web Application Firewall)やIPS(侵入防止システム)といった強固なセキュリティ機器を境界防御として導入していても、アプリケーション自体に脆弱性があれば、正規の通信経路を悪用されてシステムが侵害されます。
試験では、危険なコードをコードレビューの段階で検知し、安全なコードへ修正する実践的な能力が問われます。外部入力値をそのまま内部処理に渡す危険性や、メモリ管理の不備が引き起こす問題について、コードベースでの深い理解が求められます。
出題されるプログラミング言語の種類と特徴
過去問で出題されるプログラミング言語は、C/C++、Java、Python、ECMAScript(JavaScript)など多岐にわたります。言語ごとに発生しやすい脆弱性には明確な特徴があり、問題文を確認した瞬間に「どの言語か」を特定することが第一歩です。
C言語/C++(平成31年春期 午後Ⅱなど)では、メモリ管理をプログラマ自身が行う必要があるため、バッファオーバーフローや整数オーバーフローといったメモリ破壊系の脆弱性が頻出します。配列の境界チェックやポインタ演算、文字列操作関数(strcpyなど)の扱いに着目します。
Java(令和2年秋期 午後Ⅰなど)、Python(令和元年秋期 午後Ⅰなど)、JavaScript(令和5年春期 午後Ⅰなど)では、言語の実行環境がメモリ管理を担うためメモリ破壊系は比較的少なくなります。代わりに、SQLインジェクション、クロスサイトスクリプティング(XSS)、OSコマンドインジェクションといった、入力値の検証漏れやメタ文字のエスケープ漏れによる脆弱性が中心です。
過去問に共通する「3つの視点」
穴埋め問題を解く際、文法的に正しいコードを選ぶだけでは正解にたどり着けません。以下の3つの視点でコードを分析することが重要です。
- 入力値の検証(バリデーション):外部からのデータは常に悪意を含んでいると仮定し、ホワイトリスト方式での文字種チェックやデータ長の制限が行われているかを確認します。穴埋め箇所が
if文の条件式になっているケースが非常に多く見られます。 - 無害化(サニタイズ・エスケープ):DBクエリ・HTML出力・OSコマンド呼び出しなど、特定文脈で特別な意味を持つメタ文字を、単なる文字列として扱うよう変換する処理です。どの文字をどの関数でエスケープするかが問われます。
- 適切なエラーハンドリング:処理の失敗時に詳細なデバッグ情報を画面に出力することは、攻撃者にシステム内部情報を与えます。適切なログ記録と安全な状態へのフォールバック(フェールセーフ)が実装されているかを見極める視点が求められます。

【過去問徹底解説】SQLインジェクション対策の穴埋めパターン(令和2年秋期 午後Ⅰ 問2など)
Webアプリケーションのバックエンド処理において最も注意すべき脆弱性がSQLインジェクションです。過去問での出題頻度が特に高く、必ず押さえておくべきテーマです。
脆弱性が発生するメカニズム
SQLインジェクションは、ユーザーの入力値を文字列連結によって直接SQL文に組み込んでしまうことで発生します。以下の脆弱なコード例を見てください。
// NG:脆弱なコード例(文字列連結)
String sql = "SELECT * FROM users WHERE user_id = '" + inputId + "'";
Statement stmt = conn.createStatement();
ResultSet rs = stmt.executeQuery(sql);
攻撃者が inputId に ' OR '1'='1 を入力すると、SQL文は SELECT * FROM users WHERE user_id = '' OR '1'='1' となり、全ユーザー情報が抽出されてしまいます。過去問では、この危険な実装を安全なコードに書き換える穴埋めが出題されます。
令和2年秋期 過去問の回答例:プレースホルダの実装
令和2年秋期 午後Ⅰ 問2では、Javaを用いたSQLインジェクション対策が問われました。正解の鍵は プレースホルダ(バインド機構) の正しい実装です。
// OK:安全なコード例(PreparedStatementによるプレースホルダ)
// 穴埋め①:SQLのプレースホルダを含む文字列
String sql = "SELECT account_balance FROM accounts WHERE account_id = ?";
// 穴埋め②:PreparedStatementの生成
PreparedStatement pstmt = conn.prepareStatement(sql);
// 穴埋め③:プレースホルダへの値のバインド(型に応じたメソッドを選ぶ)
pstmt.setString(1, accountId); // 文字列型の場合
// pstmt.setInt(1, accountId); // 整数型の場合
ResultSet rs = pstmt.executeQuery();
- 穴埋め①の解法:外部入力値が入る箇所を
?に置き換えたSQL文字列を選ぶ。'を連結している選択肢は不正解。 - 穴埋め②の解法:
StatementではなくPreparedStatementを使う。この1点が正解の核心。 - 穴埋め③の解法:問題文中の変数定義を確認し、
String型ならsetString、int型ならsetIntを選ぶ。
プレースホルダを使うことで、DBエンジンが入力値を「SQLの実行命令」ではなく「単なるデータ(リテラル値)」として処理するため、悪意のあるSQL文が実行されることを構造的に防げます。
穴埋めで狙われるバインド処理と型指定のポイント
PreparedStatementのバインドメソッドは型ごとに異なります。問題文で変数の型が明記されている場合は必ずそれに従います。よく出題されるメソッドの対応は以下の通りです。
setString(index, value):String型の引数をバインドsetInt(index, value):int型の引数をバインドsetLong(index, value):long型の引数をバインド
変数をそのままSQL文字列内に連結してしまう選択肢が誤答として用意されているケースが多いため、コードの前後関係から変数のデータ型を正確に読み取る習慣をつけることが重要です。
【過去問徹底解説】ディレクトリトラバーサル対策の穴埋めパターン(令和3年秋期 午後Ⅰ 問2)
ファイルの読み書きを行う処理で頻出するのが、ディレクトリトラバーサル(パストラバーサル)の脆弱性です。
パス操作に潜む罠と相対パスの脅威
ディレクトリトラバーサルは、ファイル名を指定する入力値に ../(親ディレクトリへの移動を示す相対パス)などを含めることで、意図しないディレクトリのファイル(例:/etc/passwdやWebサーバーの設定ファイル)を不正に読み書きされる脆弱性です。
// NG:脆弱なコード例(入力値をそのままパスに使用)
char filepath[256];
snprintf(filepath, sizeof(filepath), "/var/www/html/uploads/%s", userInput);
FILE *fp = fopen(filepath, "r"); // "../../../etc/passwd" が渡されると危険
令和3年秋期 過去問の回答例:絶対パスへの変換と検証
令和3年秋期 午後Ⅰ 問2では、C++を用いたディレクトリトラバーサル対策が出題されました。対策アプローチは「パスの正規化(Canonicalization)」と「ベースディレクトリとの比較」の2段階です。
// OK:安全なコード例(realpathによる正規化 + ベースディレクトリとの前方一致チェック)
#define BASE_DIR "/var/www/html/uploads/"
// 穴埋め①:パスの正規化(realpathで絶対パスに変換)
char resolvedPath[PATH_MAX];
if (realpath(userInput, resolvedPath) == NULL) {
// エラー処理:パスの解決に失敗
return -1;
}
// 穴埋め②:ベースディレクトリとの前方一致チェック
if (strncmp(resolvedPath, BASE_DIR, strlen(BASE_DIR)) != 0) {
// ベースディレクトリ外へのアクセスを拒否
return -1;
}
// ここまで通過したパスのみ安全にファイルをオープン
FILE *fp = fopen(resolvedPath, "r");
- 穴埋め①の解法:
realpath関数(JavaならgetCanonicalPathメソッド)を呼び出す。相対パスやシンボリックリンクをすべて解決し、一意の絶対パスを返す関数を選ぶ。 - 穴埋め②の解法:
strncmpでresolvedPathの先頭がBASE_DIRと一致しているかを確認する。一致しない場合(!= 0)はエラー処理に分岐させる条件式を記述する。
../../../etc/passwdを入力されても、realpathで正規化すると/etc/passwdという絶対パスが得られます。これはBASE_DIR(/var/www/html/uploads/)で始まらないため、前方一致チェックで弾くことができます。
ベースディレクトリとの比較処理の実装ポイント
比較を行う際は以下の点に注意が必要です。
strcmp(完全一致)ではなくstrncmp(前方一致)を使用すること- 第3引数には
BASE_DIRの文字列長(strlen(BASE_DIR))を渡すこと BASE_DIRの末尾には必ず/を付けること(/var/www/html/uploadsだと/var/www/html/uploads_evil/も通過してしまうため)

【過去問徹底解説】メモリ管理とオーバーフロー(平成31年春期 午後Ⅱ 問2)
C言語やC++特有の深い知識が問われるのが、メモリ破壊系の脆弱性です。組み込みシステムや基盤ソフトウェアを題材にした問題で定期的に出題されます。
C/C++特有のメモリ管理の課題
C言語/C++では、確保したサイズを超えてデータを書き込む「バッファオーバーフロー(バッファオーバーラン)」が致命的な脆弱性となります。攻撃者によってメモリ上のリターンアドレスが書き換えられると、任意コードを実行されシステムが乗っ取られる危険性があります。
また、変数が表現できる最大値を超えた演算により値が循環して小さくなる「整数オーバーフロー」も、メモリ確保のサイズ計算を狂わせる原因として重要です。
平成31年春期 過去問の回答例①:バッファオーバーフロー対策
平成31年春期 午後Ⅱ 問2では、C++で記述されたサーバープログラムの脆弱性が取り上げられました。外部から受信したサイズ値を基にメモリを動的に確保し、データをコピーする処理における境界チェックの不備が問題となりました。
// NG:脆弱なコード例(境界チェックなし)
void process_data(char *src, size_t src_size) {
char buf[BUFFER_SIZE];
memcpy(buf, src, src_size); // src_size が BUFFER_SIZE を超えると危険
}
// OK:安全なコード例(穴埋め部分:境界チェックの追加)
void process_data(char *src, size_t src_size) {
char buf[BUFFER_SIZE];
// 穴埋め:コピー前にサイズを検証する条件式
if (src_size > sizeof(buf)) {
// バッファサイズを超える場合はエラーとして処理を中断
log_error("Buffer overflow attempt detected");
return;
}
memcpy(buf, src, src_size);
}
- 穴埋めの解法:
memcpy呼び出しの直前に、コピーするデータサイズがコピー先バッファサイズを超えていないかをif文で検証する。条件式はsrc_size > sizeof(buf)またはsrc_size >= BUFFER_SIZEとなる。「超えた場合にエラー処理へ分岐させる」というフェールセーフのロジックが正解の核心。
平成31年春期 過去問の回答例②:整数オーバーフロー対策
同問題でさらに深く問われたのが整数オーバーフロー対策です。「要素の数 count」と「1要素あたりのサイズ element_size」の積でメモリ確保サイズを計算する場面です。
// NG:脆弱なコード例(整数オーバーフローのチェックなし)
void *allocate_buffer(size_t count, size_t element_size) {
size_t total = count * element_size; // countが巨大な値だとオーバーフローの危険
return malloc(total);
}
// OK:安全なコード例(穴埋め部分:乗算前のオーバーフロー検証)
void *allocate_buffer(size_t count, size_t element_size) {
// 穴埋め:掛け算を行う前に上限を逆算して検証する条件式
if (count > SIZE_MAX / element_size) {
// 演算結果がオーバーフローする場合はエラー
return NULL;
}
size_t total = count * element_size;
return malloc(total);
}
- 穴埋めの解法:掛け算の結果でオーバーフローが起きるかを、演算を行う前に逆算して検証する。具体的には
count > SIZE_MAX / element_sizeという条件式を記述する。これにより、掛け算の結果がsize_tの上限を超えないことを保証できる。
このテクニックは「演算後の値を検証するのではなく、演算前に安全かどうかを確かめる」というセキュアコーディング特有の考え方です。過去問を通じてこの逆算パターンを確実に身につけることが重要です。
【過去問徹底解説】XSSとDOM操作のエスケープ(令和5年春期 午後Ⅰ 問3)
近年、Webフロントエンドの高度化に伴い、JavaScriptを用いたクライアントサイドのセキュアプログラミングも頻出しています。
反射型・蓄積型XSSとDOM Based XSSの違い
XSSには3種類あります。
- 反射型:リクエストパラメータがそのままレスポンスに出力されるタイプ
- 蓄積型:DBに保存されたスクリプトが別ユーザーの画面に出力されるタイプ
- DOM Based XSS:サーバーサイドを介さず、ブラウザ上のJavaScript処理によりDOMを操作する過程で発生するタイプ
特に最近の過去問で狙われやすいのがDOM Based XSSです。クライアントサイドのコードの穴埋め問題として出題されます。
令和5年春期 過去問の回答例:安全なプロパティの選択
令和5年春期 午後Ⅰ 問3では、URLのフラグメント(#以降の文字列)などから取得したユーザー入力値を画面に表示する処理におけるDOM Based XSS対策が問われました。
// NG:脆弱なコード例(innerHTML を使用)
const userInput = location.hash.slice(1); // URLフラグメントを取得
document.getElementById("output").innerHTML = userInput;
// 攻撃者が #<img src=x onerror=alert(1)> を入力するとスクリプトが実行される
// OK:安全なコード例(穴埋め部分:textContent に変更)
const userInput = decodeURIComponent(location.hash.slice(1));
// 穴埋め:HTMLとして解釈させないプロパティを選ぶ
document.getElementById("output").textContent = userInput;
// textContent はHTMLタグを自動エスケープしてテキストとして表示する
- 穴埋めの解法:
innerHTMLをtextContent(またはinnerText)に変更する。innerHTMLはHTML文字列として解釈するため脆弱だが、textContentは文字列を純粋なテキストとして扱い、<script>タグなどのHTMLタグを自動的にエスケープして表示する。
この問題は「同じ値を代入していても、使用するプロパティの違いで脆弱性の有無が変わる」という点がポイントです。フレームワークや言語が提供している「安全なAPI」を適切に選択する知識が繰り返し問われます。
サーバーサイドXSS対策との比較
DOM Based XSSと合わせて、サーバーサイド(Java/Python)で行うHTMLエスケープのコードも押さえておきましょう。
// Java:HTMLエスケープの例(OWASP Java Encoder使用)
// 穴埋め:ユーザー入力をHTMLとして出力する際にエスケープを行う
String safeOutput = Encode.forHtml(userInput);
response.getWriter().println("<p>" + safeOutput + "</p>");
// Python(Flask/Jinja2):テンプレートエンジンの自動エスケープを活用
// {{ user_input }} ← Jinja2はデフォルトでエスケープ済み(安全)
// {{ user_input | safe }} ← safe フィルタは使用禁止(脆弱)
Jinja2テンプレートで | safe フィルタをエスケープ解除の目的で不用意に使用したコードが提示され、それを安全な形に修正する穴埋めも出題されています。

穴埋め問題を確実に正解するための学習ステップ
変数の型とスコープを意識したコードリーディング
空白部分だけを見ていても正解は分かりません。穴埋め箇所の前後にあるコードを丁寧に読み、変数が表すもの・データ型・スコープを正確に把握します。
変数名が userId であれば整数型や文字列型の識別子、isSuccess であれば真偽値(boolean)と推測できます。直前で呼び出されている関数の戻り値が変数に代入されている場合は、その関数仕様から変数の意味を確定させます。空白部分が条件式の内部であれば、条件分岐によって「正常系ルート」と「異常系ルート」のどちらに進ませるかをプログラム全体の目的から逆算して明確にします。文脈を理解することで、入るべきコードの候補が論理的に絞り込まれていきます。
ライブラリ関数の仕様(戻り値と例外)を熟読する
問題文には、コード内で使用されている関数の仕様が表や説明文として提示されます。過去問の穴埋め問題を解く最大の鍵は、この仕様説明の注釈に隠されていることが多いです。関数が受け取る引数・成功時の戻り値・失敗時の戻り値(NULLやエラーコード -1 など)を熟読します。
空白部分が関数の戻り値を受け取った直後の判定処理であれば、必ず失敗時の戻り値をチェックするコードが入ります。「関数は常に成功するとは限らない」という原則に則り、仕様書に記載された異常系の戻り値を漏れなくキャッチするコードを記述する習慣をつけてください。
過去問のコードを実際にトレースする
学習の総仕上げとして、過去問のコードを頭の中や紙の上で実際にトレース(実行順序を追跡)する習慣をつけてください。正常な値が入力された場合だけでなく、「攻撃者が ' OR 1=1 -- や ../../../etc/passwd を入力した場合、変数の値はどう変化し、どのif文でブロックされるべきか」をシミュレーションします。
この攻撃者視点のコードトレースを行うことで、なぜその穴埋め部分にそのコードが必要なのかが深く理解でき、単なる暗記ではない本物の応用力が身につきます。
頻出パターン別:回答コードのチートシート
本記事で解説した過去問パターンを、すぐに参照できるようまとめます。
SQLインジェクション対策(Java / JDBC)
| 脆弱なコード | 安全なコード(穴埋め答え) |
|---|---|
Statement | PreparedStatement |
"... WHERE id = '" + input + "'" | "... WHERE id = ?" |
| ―(バインドなし) | pstmt.setString(1, input) |
ディレクトリトラバーサル対策(C言語)
| 処理 | 穴埋め答え |
|---|---|
| パスの正規化 | realpath(userInput, resolvedPath) |
| 前方一致チェック | strncmp(resolvedPath, BASE_DIR, strlen(BASE_DIR)) != 0 |
バッファオーバーフロー対策(C言語)
| 処理 | 穴埋め答え |
|---|---|
| コピー前の境界チェック | if (src_size > sizeof(buf)) |
| 整数オーバーフロー検証 | if (count > SIZE_MAX / element_size) |
XSS対策(JavaScript / DOM)
| 脆弱なコード | 安全なコード(穴埋め答え) |
|---|---|
.innerHTML = userInput | .textContent = userInput |
{{ value | safe }} | {{ value }} |
まとめ
セキュアプログラミングの穴埋め問題は、入力値の検証・エスケープ処理による無害化・フェールセーフを意識した適切なエラーハンドリングという情報セキュリティの基本原則を、実際のソースコード上で実現できるかを問う設問です。
過去問を解く際は、解答例のコードを丸暗記するのではなく、「なぜそのコードが必要なのか」「そのコードが欠落していたらどのような脆弱性が発生し、システムにどのような被害をもたらすのか」というメカニズムまで深く理解することが合格への最短ルートです。
コードの文脈を丁寧に読み解く論理的な思考力を養い、いかなる言語やシチュエーションが出題されても確実に得点できる実戦力を身につけていきましょう。