こんにちは、NTTテクノクロスの物江です。
この記事はNTTテクノクロス Advent Calendar 2025シリーズ2の6日目の記事です。
はじめに
生成AI時代、コードは"書ける"だけでは足りない
近年、生成AIの登場により、コードを書く作業は大きく変わりました。開発効率は飛躍的に向上していますが、ただ「動くコード」を作るだけでは不十分です。
実際、レガシー化したソースコードで見られた脆弱性が、生成AIが作成したコードでも再現されるケースがありました。それが本記事を執筆するきっかけです。
本記事では、生成AIが書いたコードの安全性や再現性、メンテナンス性に焦点を当て、安全なコードを作るためのポイントを解説します。
読者ターゲット
以下のような方におすすめです。
- 独学でコードを書いている方
- 生成AIで作成したコードを本番環境へ適用することを検討している方
- 生成AIコードの安全性・品質に関心のある方
背景:生成AIとセキュリティの交差点
近年、企業のシステム停止や情報漏洩が相次ぎました。特に以下のような傾向が見られます。
- 外部委託先や自動化された開発環境を経由した侵入の増加
- サプライチェーン攻撃の高度化
- 開発速度優先によるセキュリティレビューの形骸化
生成AIコードも例外ではなく、開発速度と安全性のバランスを取るためのセキュリティ設計の見直しが求められています。
次章では、生成AIが生成したコードに潜む典型的な落とし穴を具体例で見ていきます。
生成AIコードの落とし穴
生成AIは「動くコード」を優先する傾向があり、例えば以下のような問題が発生しやすいです。
1. 入力検証の不足
# 生成AIが書きがちなコード例
def get_user(user_id):
query = f"SELECT * FROM users WHERE id = {user_id}"
return db.execute(query)
このコードはSQLインジェクションの脆弱性があります。
問題点:
- ユーザー入力を直接SQL文に埋め込んでいる
- user_id に "1 OR 1=1" のような文字列を渡すと、全ユーザー情報が取得できてしまう
- "1; DROP TABLE users--" のような破壊的なコマンドも実行可能
- 型チェックがない
- 数値を期待しているが、文字列や特殊文字が入力される可能性を考慮していない
- エスケープ処理がない
- 悪意のある入力をそのまま実行してしまう
改善例:
SQLインジェクション対策はいくつかありますが、本記事では最も基本的で効果的なプレースホルダーを使用します。
プレースホルダー(?)を使うことで、入力値が「データ」として扱われ、「コマンド」として実行されなくなるため、悪意のある入力の実行を防げます。
# プレースホルダーを使った安全な実装
def get_user(user_id):
# 1. 入力値の型チェック
try:
user_id = int(user_id)
except (ValueError, TypeError):
raise ValueError("user_id must be an integer")
# 2. プレースホルダーを使用(SQLインジェクション対策)
query = "SELECT * FROM users WHERE id = ?"
return db.execute(query, (user_id,))
2. 認証・認可処理の省略
入力検証の不足だけでなく、アクセス制御の欠如も重大なリスクです。
生成AIコードはプロトタイプとして動作することを優先し、認証チェックを省略することがあります。
# 危険な例
@app.route('/admin/users')
def admin_users():
return render_template('users.html', users=get_all_users())
問題点:
- 誰でもアクセス可能
- URLを知っていれば、ログインしていないユーザーでも管理画面にアクセスできる
- /admin/usersに直接アクセスするだけで全ユーザー情報が閲覧可能
- 権限チェックがない
- 一般ユーザーと管理者の区別がない
- 本来は管理者のみアクセスすべき機能が誰でも使える状態
- 情報漏洩のリスク
- 個人情報や機密情報が無防備に公開される
改善例:
-
@login_requiredでログイン状態を確認 -
@admin_requiredで管理者権限を確認 - 権限がない場合は403エラーを返す
- 2段階チェックで多層防御を実現
@app.route('/admin/users')
@login_required # 1. ログインチェック
@admin_required # 2. 管理者権限チェック
def admin_users():
return render_template('users.html', users=get_all_users())
生成AIは「動くコード」を優先するため、こうした認証・認可処理を後回しにしがちです。
しかし、本番環境では最初から実装すべき必須要件です。
3. エラーハンドリングの不備
例外処理が不十分で、機密情報がエラーメッセージに含まれるケースがあります。特にログに個人情報が含まれていた場合、個人情報漏洩のリスクがあります。
# 危険な例
def login(username, password):
try:
user = db.query(f"SELECT * FROM users WHERE username='{username}'")
if user.password == password:
return user
except Exception as e:
# エラー内容をそのまま表示
print(f"Error: {e}")
return f"Login failed: {e}"
問題点:
- 機密情報の漏洩
- データベースのテーブル構造やカラム名がエラーメッセージに含まれる
- Error: Table 'users' doesn't exist のような情報が攻撃者に渡る
- デバッグ情報の露出
- スタックトレースに内部のファイルパスやコード構造が表示される
- 本番環境でこれらの情報が見えると、攻撃の手がかりになる
- ユーザー体験(UX)の悪化
- 技術的なエラーメッセージはユーザーには理解できない
- 「何が問題なのか」「どうすればいいのか」が分からない
- ログに個人情報が記録される
- パスワードやメールアドレスがログファイルに平文で残る可能性
改善例:
- エラーメッセージの分離
- ユーザー向け:分かりやすく、機密情報を含まない
- 開発者向け:詳細なログを安全な場所に記録
- 例外の種類ごとに処理を分ける
- DatabaseError、ValueError など、具体的な例外をキャッチ
- Exception での一括キャッチは最後の砦として使用
- ログレベルの使い分け
- info: 正常な動作の記録
- error: 回復可能なエラー
- critical: システムに重大な影響を与えるエラー
- 個人情報をログに含めない
- パスワードは絶対にログに出力しない
- ユーザー名も必要最小限に
# 改善例
# ロガーの設定
logger = logging.getLogger(__name__)
def login(username, password):
try:
# プレースホルダーを使用(SQLインジェクション対策)
user = db.query("SELECT * FROM users WHERE username=?", (username,))
if not user:
# ユーザー向けには一般的なメッセージ
return {"success": False, "message": "ログインに失敗しました"}
if user.verify_password(password): # ハッシュ化されたパスワードと比較
logger.info(f"User logged in: {username}") # 個人情報を含まないログ
return {"success": True, "user": user}
else:
return {"success": False, "message": "ログインに失敗しました"}
except DatabaseError as e:
# 開発者向けには詳細をログに記録
logger.error(f"Database error during login: {type(e).__name__}", exc_info=True)
# ユーザー向けには一般的なメッセージ
return {"success": False, "message": "システムエラーが発生しました。しばらくしてから再度お試しください"}
except Exception as e:
# 予期しないエラー
logger.critical(f"Unexpected error during login: {type(e).__name__}", exc_info=True)
return {"success": False, "message": "システムエラーが発生しました"}
生成AIコードは「エラーが起きたら表示する」というシンプルな実装をしがちですが、本番環境ではセキュリティとユーザー体験(UX)の両立が必要です。
4. 存在しないライブラリの参照(ハルシネーションの悪用)
生成AIが実在しないライブラリ名を使用することがあります。これはサプライチェーン攻撃の入り口となる危険があります。
# 生成AIが提案した危険なコード例
import secure_crypto_utils # 実在しないライブラリ
def encrypt_data(data):
return secure_crypto_utils.encrypt(data, algorithm="AES256")
攻撃の流れ:
- 生成AIが存在しないライブラリ名(ここでは
secure_crypto_utils)を提案 - 開発者がライブラリを検証せずインストールを試みる
- 攻撃者が事前に同名のマルウェアパッケージを登録
- マルウェア感染し、企業ネットワークへの侵入経路になる
実際、PyPIで攻撃者が人気のある正規パッケージと似た名前の悪意のあるパッケージを登録するタイポスクワッティング攻撃も起きています。
対策:
-
インストール前の確認
- 公式ドキュメントで存在を確認
- GitHubリポジトリなどがあるか確認
- PyPI公式サイトでダウンロード数や更新日を確認
- パッケージ情報の確認
pip show パッケージ名
- 公式ドキュメントで存在を確認
-
依存関係の固定
- requirements.txt でバージョンを固定
requests==2.31.0 # バージョン指定
- requirements.txt でバージョンを固定
-
セキュリティツールの活用
-
pip-audit:脆弱性チェック -
safety:既知の脆弱性スキャン
-
-
その他
- プライベートリポジトリの利用(企業内で承認されたパッケージのみ)
- サンドボックスの利用(本番環境に入れる前に検証)
- コードレビューの徹底(生成AIが提案したライブラリは特に注意)
生成AIは「それらしい名前」のライブラリを自信満々に提案することがあります。必ず公式ドキュメントで実在を確認してからインストールしましょう。
5. 冗長なロジック
生成AIは動作するコードを生成できますが、効率性や保守性を考慮しない冗長なコードを出力することがあります。
元々あったコードを参照する際、複雑性が高いほど冗長性が高くなる傾向にあるようです。
# 生成AIが提案した冗長なコード
def get_user_status(user):
if user.is_active == True:
if user.is_verified == True:
if user.has_subscription == True:
return "premium"
else:
return "free"
else:
return "unverified"
else:
return "inactive"
# リストから偶数を抽出
def get_even_numbers(numbers):
result = []
for i in range(len(numbers)):
if numbers[i] % 2 == 0:
result.append(numbers[i])
return result
問題点:
- 可読性の低下
- ネストが深く、ロジックの把握が困難
- 条件分岐が複雑で修正時にバグを生みやすい
- パフォーマンスの悪化
- 不要なループや条件分岐が多い
- 保守性の低下
- 新しい条件を追加する際に全体を書き換える必要がある
- テストケースが増える
改善例:
# シンプルで保守性の高いコード
def get_user_status(user):
if not user.is_active:
return "inactive"
if not user.is_verified:
return "unverified"
return "premium" if user.has_subscription else "free"
対策:
- コードレビューの実施 - 「動く」だけでなく「読みやすい」かを確認
- リファクタリングの習慣 - 生成AIのコードをそのまま使わず、必ず見直す
- 静的解析ツールの活用 - 複雑度やコード品質をチェック
- パフォーマンステスト - 処理速度を測定して比較
生成AIは「動くコード」を作るのは得意ですが、「良いコード」を書くとは限りません。 可読性・保守性・パフォーマンスの観点から必ずチェック・レビューを行いましょう。
6. 未使用関数(デッドコード)
生成AIは要求された機能を実装する際、実際には使われない関数やコードを一緒に生成することがあります。これらのデッドコードは、コードベースを肥大化させ、保守性を低下させます。
例えば、「ユーザーを追加・削除する機能」を依頼しただけなのに、ユーザーの更新やEメール情報追加など、使わない機能・関数まで「親切に」実装してしまうケースがあります。
問題点:
- コードベースの肥大化
- 不要なコードが増え、プロジェクト全体の見通しが悪くなる
- 保守コストの増加
- 使われていない関数もメンテナンスが必要になる可能性
- リファクタリング時に「これは使われているのか?」の判断が困難
- 誤った安心感・セキュリティリスク
- 未使用の関数に脆弱性があっても気づきにくい
- 将来的に誤って使用される可能性
対策:
-
必要最小限の実装を明示的に指示
- 生成AIへのプロンプトで「今必要な機能だけ」と明記
- 「将来の拡張性」を理由に余分な機能を入れない
-
コードレビューで未使用コードをチェック
- 「この関数は今使われているか?」を確認
- 使われていなければ削除する
-
静的解析ツールで検出
-
vulture、pylintなどで未使用コードを定期的にチェック
-
-
必要になったら実装する
- 「後で必要になるかも」ではなく「今必要か」で判断
- 生成AIはいつでも追加実装できる
生成AIは「あると便利そうな機能」を先回りして実装することがあります。
今必要な機能だけを実装し、使われなくなった機能は削除しましょう。
7. ハードコードされた機密情報
生成AIは動作するコードを優先するため、APIキーやパスワードなどの機密情報を直接コードに埋め込むことがあります。これは重大なセキュリティリスクです。
生成AIは「動作する例」を示すために、サンプルとして機密情報をハードコードしてしまいます。開発者がそのまま本番環境で使用すると、情報漏洩につながります。
import requests
def fetch_user_data():
# APIキーが直接埋め込まれている
api_key = "api-proj-abc123xyz789"
headers = {"Authorization": f"Bearer {api_key}"}
response = requests.get("https://api.example.com/users", headers=headers)
return response.json()
このコードをGitHubにプッシュすると、誰でもあなたのAPIキーを見ることができます。
改善例:
- 環境変数を使用する
- .envファイルで管理し、.gitignoreに追加
# pythonのソース import os from dotenv import load_dotenv load_dotenv() # .envファイルから読み込み def fetch_user_data(): api_key = os.getenv("API_KEY") headers = {"Authorization": f"Bearer {api_key}"} response = requests.get("https://api.example.com/users", headers=headers) return response.json()# .envファイル API_KEY=api-proj-abc123xyz789# .gitignore .env - 生成AIに明示的に指示する
- 「環境変数を使用してください」と明記
- コードレビューで必ずチェック
- 機密情報がハードコードされていないか確認
- 既にコミットしてしまった場合
- 該当の認証情報を即座に無効化・変更
生成AIが提案したコードに機密情報が含まれていないか、必ず確認してから使用しましょう。 セキュリティは後から対応するのではなく、最初から正しく実装することが重要です。
8. 今後の課題と継続的対策
ここまでご紹介した問題点以外にも、生成AIコードには様々な課題があります。
また、生成AI技術は発展途上のため、新たなセキュリティリスクや品質問題が今後も発生する可能性があります。
定期的に以下のガイドラインをチェックするなど、セキュリティリスクに備えておくことが大切です。
参考ガイドライン
-
IPA 安全なウェブサイトの作り方
- IPAが展開している安全なウェブサイトを作る上で実践的なガイドライン
- 数少ない日本語のガイドライン
-
OWASP Top 10 for LLM Applications
- LLM(大規模言語モデル)アプリケーションのセキュリティリスクトップ10
- 各組織のAI利用ガイドライン
- 所属組織や業界団体が公開しているガイドラインも確認
生成AIは強力なツールですが、最終的な責任は開発者にあります。
この記事で紹介した問題点を意識しながら、生成AIを賢く活用していきましょう。
将来の展望
生成AIの進化は非常に速く、セキュリティを考慮したコード生成能力も向上しています。将来、これまでご紹介した課題の多くは技術的に解決される可能性があります。
しかし、どれだけAIが進化しても、最終的な説明責任は開発者・組織にあります。
- 「生成AIが書いたから」は免責理由にならない
- コードの品質・安全性に対する説明責任は人間が負う
- 生成AIはツールであり、安全性などを最後に担保するのは人間が行う
この原則は今後も変わらないでしょう。
まとめ
生成AIは強力な開発支援ツールですが、「動くコード」と「安全なコード」は別物です。
✅生成AIコードには脆弱性や品質問題が潜む可能性があるため、検証せずそのまま公開しないこと
✅セキュリティガイドライン(IPA、OWASPなど)を活用した検証が重要
✅最終的な責任や判断は人間にあり、適切なレビュープロセスが必須
生成AIで開発効率を上げつつ、人間の知見で安全性を担保する——このバランスが、これからの開発に求められます。
それでは引き続き、NTTテクノクロス Advent Calendar 2025をお楽しみください。
参考資料
セキュリティガイドライン:
-
IPA 安全なウェブサイトの作り方
- 日本語で読みやすい実践的なガイドライン
- 具体的な脆弱性と対策がセットで解説されている
-
OWASP Top 10:2025 RC1
- Webアプリケーションの代表的な脆弱性トップ10
- 世界中で最も参照されているセキュリティガイドライン
-
NIST AI Risk Management Framework
- AIシステムのリスク管理フレームワーク
-
CWE (Common Weakness Enumeration)
- ソフトウェアの脆弱性パターン集