プログラミング入門

Python・PHP・JavaScriptで起きがちなセキュリティ&パフォーマンスのミス、あなたのコードは大丈夫?

「自分のコードは大丈夫だと思ってたのに、気づいたら重大な脆弱性が…」

そんな経験、あるいは「いつかやらかしそうで怖い」という不安、持っていませんか? 実はセキュリティインシデントやサイトダウンの多くは、高度な攻撃や特殊なサーバー障害が原因ではなく、コードの中にひっそり潜んでいた「うっかりミス」が引き金になっているんですよね。

今回はPython・PHP・JavaScriptのプロジェクトに共通して起きがちな、セキュリティとパフォーマンスの隠れた落とし穴を整理してみました。初〜中級者の方にぜひ読んでほしい内容です! 🔍

セキュリティとパフォーマンスは「別の話」じゃない

code security vulnerability
code security vulnerability / Photo by AI25.Studio Studio via Pexels

よくある誤解が、「セキュリティはセキュリティチームが、パフォーマンスはインフラチームが対応する話」という分け方です。でも実際には、この2つは密接に絡み合っています

たとえば、入力値の検証が甘いコードは、SQLインジェクションの温床になるだけでなく、無駄なDBクエリを大量発行してパフォーマンスも落とします。つまり、一つのミスが両方の問題を同時に引き起こすことが多いんです。

この記事では「セキュリティ問題」「パフォーマンス問題」を分けて考えるのではなく、コードの品質を上げることで両方まとめて解決するという視点でミスを紹介していきます。

よくあるミス① Pythonの入力検証不足(SQLインジェクション)

Pythonでユーザー入力をそのままDBに渡してしまうコード、見たことありませんか? 🚨

# ❌ 危険な例(SQLインジェクションが起きる)
import sqlite3

def get_user(username):
    conn = sqlite3.connect('app.db')
    cursor = conn.cursor()
    query = f"SELECT * FROM users WHERE username = '{username}'"
    cursor.execute(query)
    return cursor.fetchone()

# 攻撃者が username に「' OR '1'='1」を入力すると全件取得される!

これは古典的なSQLインジェクションの例ですが、今でも実際のコードで見かけることがあります。f文字列でクエリを組み立てるのは絶対にNGです。

# ✅ 安全な例(プレースホルダーを使う)
import sqlite3

def get_user(username):
    conn = sqlite3.connect('app.db')
    cursor = conn.cursor()
    query = "SELECT * FROM users WHERE username = ?"
    cursor.execute(query, (username,))  # タプルで渡す
    return cursor.fetchone()

? のようなプレースホルダーを使うと、ライブラリ側が自動的にエスケープ処理をしてくれます。SQLAlchemyなどのORMを使う場合も同様に、生のSQL文字列結合は避けましょう。

よくあるミス② Pythonの「eval()」「pickle」の無警戒な使用

「文字列をコードとして実行したい」というシーンで eval() に頼りたくなることがありますが、これは非常に危険です。

# ❌ 危険な例
user_input = input("計算式を入力してください: ")
result = eval(user_input)  # "__import__('os').system('rm -rf /')" なども実行できてしまう!
print(result)
# ✅ 数式計算だけしたいなら ast.literal_eval を使う
import ast

def safe_eval(expression):
    try:
        # リテラル(数値・文字列・リスト等)のみ評価可能
        return ast.literal_eval(expression)
    except (ValueError, SyntaxError):
        return None

result = safe_eval("[1, 2, 3]")  # OK
result = safe_eval("__import__('os')")  # None が返る(安全)

同様に、pickle モジュールも注意が必要です。信頼できないソースから受け取ったデータを pickle.loads() で復元すると、任意コードが実行されるリスクがあります。外部から受け取るデータのシリアライズにはJSONを使うのが基本です。

よくあるミス③ PHPの出力エスケープ忘れ(XSS)

PHPでユーザーの入力をそのままHTMLに出力していませんか? これはXSS(クロスサイトスクリプティング)の典型的な原因です。

<?php
// ❌ 危険な例
$name = $_GET['name'];
echo "こんにちは、" . $name . "さん!";
// name に <script>alert('XSS')</script> を入れられると実行される
?>
<?php
// ✅ 安全な例
$name = $_GET['name'];
echo "こんにちは、" . htmlspecialchars($name, ENT_QUOTES, 'UTF-8') . "さん!";
// <script> タグが &lt;script&gt; に変換されて無害化される
?>

htmlspecialchars() を使うだけで、<> などの特殊文字がHTMLエンティティに変換され、スクリプトとして実行されなくなります。画面に出力するすべての外部データに対して、この処理を忘れずに。

よくあるミス④ JavaScriptの「innerHTML」の多用(XSS)

JavaScriptでも、HTMLを動的に書き換えるときに innerHTML を使うとXSSの危険があります。

// ❌ 危険な例
const userComment = getUserInput(); // 外部から取得した文字列
document.getElementById('comment').innerHTML = userComment;
// <img src=x onerror='alert(1)'> などが実行されてしまう
// ✅ 安全な例(テキストとして扱う)
const userComment = getUserInput();
document.getElementById('comment').textContent = userComment;
// textContent はHTMLとして解釈されず、文字列がそのまま表示される

単純なテキスト表示なら textContent を使いましょう。どうしてもHTMLを挿入する必要がある場合は、DOMPurifyなどのライブラリでサニタイズしてから使います。

よくあるミス⑤ Pythonのループ内でDB接続を繰り返す(N+1問題)

今度はパフォーマンス系のミスです。N+1問題とは、ループのたびに毎回DBへ問い合わせてしまうパターンのこと。件数が増えると一気に遅くなります。

# ❌ 遅い例(N+1問題)
def get_all_user_posts(user_ids):
    results = []
    for user_id in user_ids:  # 100人いたら100回クエリが走る!
        posts = db.query(f"SELECT * FROM posts WHERE user_id = {user_id}")
        results.append(posts)
    return results
# ✅ 速い例(IN句でまとめて取得)
def get_all_user_posts(user_ids):
    # プレースホルダーを動的に生成
    placeholders = ','.join(['?' for _ in user_ids])
    query = f"SELECT * FROM posts WHERE user_id IN ({placeholders})"
    posts = db.query(query, user_ids)  # 1回のクエリで全件取得!
    return posts

SQLの IN 句を使うことで、100回のクエリが1回になります。ORMを使っている場合は select_related()(Django)や joinedload()(SQLAlchemy)などの機能でN+1を防げます。

よくあるミス⑥ JavaScriptのイベントリスナー登録漏れ(メモリリーク)

JavaScriptのパフォーマンス問題でよくあるのが、不要になったイベントリスナーを削除しないケースです。SPAやReactを使っている場合は特に注意が必要です。

// ❌ 問題のある例(コンポーネントが消えてもリスナーが残り続ける)
function setupScrollListener() {
    window.addEventListener('scroll', () => {
        console.log('scrolled!');
    });
}

// ページ遷移のたびに呼ばれると、リスナーが積み重なってメモリリークに
// ✅ 良い例(クリーンアップ関数を用意する)
function setupScrollListener() {
    const handleScroll = () => {
        console.log('scrolled!');
    };

    window.addEventListener('scroll', handleScroll);

    // クリーンアップ関数を返す
    return function cleanup() {
        window.removeEventListener('scroll', handleScroll);
    };
}

// Reactの場合はuseEffectの戻り値として返す
// useEffect(() => {
//     const cleanup = setupScrollListener();
//     return cleanup;
// }, []);

よくあるミス⑦ 秘密情報(APIキー・パスワード)のハードコード

これはPython・PHP・JavaScript共通で絶対にやってはいけないミスです。コードにAPIキーやパスワードを直書きして、うっかりGitHubにpushしてしまうケースが後を絶ちません。

# ❌ 絶対にダメな例
API_KEY = "sk-abcdefg1234567890"  # GitHubに上げたら終わり
DB_PASSWORD = "mysecretpassword123"

def connect_to_api():
    return requests.get("https://api.example.com", headers={"Authorization": API_KEY})
# ✅ 正しい例(環境変数から読み込む)
import os
from dotenv import load_dotenv

load_dotenv()  # .envファイルを読み込む

API_KEY = os.getenv("API_KEY")
DB_PASSWORD = os.getenv("DB_PASSWORD")

def connect_to_api():
    return requests.get("https://api.example.com", headers={"Authorization": API_KEY})

python-dotenv ライブラリを使えば、.env ファイルに秘密情報をまとめて管理できます。.env ファイルは必ず .gitignore に追加してリポジトリに含めないようにしましょう。

# .env ファイル(リポジトリには絶対に含めない!)
API_KEY=sk-abcdefg1234567890
DB_PASSWORD=mysecretpassword123
# .gitignore に追加
.env
*.env
.env.local

よくあるミス⑧ PHPのファイルアップロードの検証不足

PHPでファイルアップロード機能を作るとき、拡張子だけチェックして安心していませんか? 拡張子は偽装できるので、それだけでは不十分です。

<?php
// ❌ 危険な例(拡張子チェックだけでは不十分)
$ext = pathinfo($_FILES['upload']['name'], PATHINFO_EXTENSION);
if ($ext === 'jpg') {
    move_uploaded_file($_FILES['upload']['tmp_name'], 'uploads/' . $_FILES['upload']['name']);
}
?>
<?php
// ✅ 安全な例(MIMEタイプも確認する)
$allowed_types = ['image/jpeg', 'image/png', 'image/gif'];
$file_info = finfo_open(FILEINFO_MIME_TYPE);
$mime_type = finfo_file($file_info, $_FILES['upload']['tmp_name']);
finfo_close($file_info);

if (in_array($mime_type, $allowed_types)) {
    // ファイル名もサニタイズしてから保存
    $safe_name = basename($_FILES['upload']['name']);
    $safe_name = preg_replace('/[^a-zA-Z0-9._-]/', '_', $safe_name);
    move_uploaded_file($_FILES['upload']['tmp_name'], 'uploads/' . $safe_name);
} else {
    echo '許可されていないファイル形式です';
}
?>

MIMEタイプの確認に加えて、アップロード先のディレクトリがWebから直接実行できない場所に設定されているかも確認しましょう。

チェックリストでまとめ

今回紹介したミスを、チェックリスト形式でまとめます。自分のコードを振り返るときに使ってみてください 📋

チェック項目言語種別
DBクエリにプレースホルダーを使っているPython・PHPセキュリティ
eval() や pickle を外部データに使っていないPythonセキュリティ
HTML出力時に htmlspecialchars() を使っているPHPセキュリティ
DOM操作に textContent を優先しているJavaScriptセキュリティ
ループ内でDBクエリを繰り返していないPython・PHPパフォーマンス
不要なイベントリスナーを削除しているJavaScriptパフォーマンス
APIキー・パスワードを環境変数で管理している全言語共通セキュリティ
ファイルアップロードでMIMEタイプも検証しているPHPセキュリティ

まとめ:「動いてるから大丈夫」は危ない

今回紹介したミスの多くは、コードが「動いている」状態でも問題が表面化しないのが怖いところです。SQLインジェクションもXSSも、普通に使っている限りエラーにはなりません。攻撃者が悪意を持って試みたとき、初めて被害が起きます。

大切なのは、コードを書くときに「このデータはどこから来たものか?」「外部からどんな値が入り得るか?」を常に意識する習慣です。

  • 外部からの入力は必ず検証・エスケープしてから使う
  • 秘密情報はコードに書かず環境変数で管理する
  • 「動く」だけでなく「安全に動く」を意識してコードを書く

一つひとつは小さな習慣ですが、積み重ねることでコードの品質は大きく変わります。ぜひ今日から自分のコードを見直してみてください! 💪

📚 関連商品・おすすめ書籍

スッキリわかるPython入門 第2版 (スッキリわかる入門シリーズ)

もしも

スッキリわかるPython入門 第2版 (スッキリわかる入門シリーズ)

初心者に定番のPython入門書

Amazonで見る
徹底攻略! 電子工作&プログラミング Arduinoで学ぶ電子工作完全ガイド

もしも

徹底攻略! 電子工作&プログラミング Arduinoで学ぶ電子工作完全ガイド

電子工作とプログラミングを同時に学べる

Amazonで見る
実践Claude Code入門―現場で活用するためのAIコーディングの思考法

もしも

実践Claude Code入門―現場で活用するためのAIコーディングの思考法

AIコーディングの現場活用法を学ぶ一冊

Amazonで見る

※本記事にはアフィリエイトリンクが含まれます。

ABOUT ME
やまちゃん
これまで学生と社会人を合わせて5000人以上にプログラミング学習を指導。 ゼロからイチをわかりやすく解説する専門家として活動しており、本業ではArduinoを用いたIoT開発とロボットプログラミングが専門。 Pythonを用いたアプリ開発、ウェブアプリケーションの開発で業務の効率化をサポートしています。

COMMENT

メールアドレスが公開されることはありません。 が付いている欄は必須項目です