「自分のコードは大丈夫だと思ってたのに、気づいたら重大な脆弱性が…」
そんな経験、あるいは「いつかやらかしそうで怖い」という不安、持っていませんか? 実はセキュリティインシデントやサイトダウンの多くは、高度な攻撃や特殊なサーバー障害が原因ではなく、コードの中にひっそり潜んでいた「うっかりミス」が引き金になっているんですよね。
今回はPython・PHP・JavaScriptのプロジェクトに共通して起きがちな、セキュリティとパフォーマンスの隠れた落とし穴を整理してみました。初〜中級者の方にぜひ読んでほしい内容です! 🔍
- セキュリティとパフォーマンスは「別の話」じゃない
- よくあるミス① Pythonの入力検証不足(SQLインジェクション)
- よくあるミス② Pythonの「eval()」「pickle」の無警戒な使用
- よくあるミス③ PHPの出力エスケープ忘れ(XSS)
- よくあるミス④ JavaScriptの「innerHTML」の多用(XSS)
- よくあるミス⑤ Pythonのループ内でDB接続を繰り返す(N+1問題)
- よくあるミス⑥ JavaScriptのイベントリスナー登録漏れ(メモリリーク)
- よくあるミス⑦ 秘密情報(APIキー・パスワード)のハードコード
- よくあるミス⑧ PHPのファイルアップロードの検証不足
- チェックリストでまとめ
- まとめ:「動いてるから大丈夫」は危ない
セキュリティとパフォーマンスは「別の話」じゃない

よくある誤解が、「セキュリティはセキュリティチームが、パフォーマンスはインフラチームが対応する話」という分け方です。でも実際には、この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> タグが <script> に変換されて無害化される
?>
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も、普通に使っている限りエラーにはなりません。攻撃者が悪意を持って試みたとき、初めて被害が起きます。
大切なのは、コードを書くときに「このデータはどこから来たものか?」「外部からどんな値が入り得るか?」を常に意識する習慣です。
- 外部からの入力は必ず検証・エスケープしてから使う
- 秘密情報はコードに書かず環境変数で管理する
- 「動く」だけでなく「安全に動く」を意識してコードを書く
一つひとつは小さな習慣ですが、積み重ねることでコードの品質は大きく変わります。ぜひ今日から自分のコードを見直してみてください! 💪





