6月23日、Stack Overflowが「Your AI shipped a backend that boots. That is the whole problem.」と題した記事を公開した。AIが生成したバックエンドが「起動して200を返す」だけで完成扱いになる問題と、フレームワーク設計によってその罠を防ぐアプローチが詳説されている。テストが緑でもセキュリティホールは塞がれていない——その構造的な問題を、筆者はフレームワークの設計思想そのもので解こうとしている。
「テストが通った」は「安全」ではない
AIコーディングツールに「ExpressでNode APIを作って」と依頼すると、だいたい以下のようなコードが返ってくる。
import express from "express";
import cors from "cors";
const app = express();
app.use(express.json()); // サイズ制限なし
app.use(cors()); // すべてのオリジンを許可
app.post("/fetch-cover", async (req, res) => {
const r = await fetch(req.body.url); // SSRFの入口
const buf = await r.arrayBuffer();
res.set("content-type", r.headers.get("content-type"));
res.send(Buffer.from(buf));
});
app.post("/books", (req, res) => {
db.insert(req.body); // 認証なし、バリデーションなし
res.json(req.body);
});
app.listen(3000);
このコードはテストを通過する。200を返す。1時間以内にデプロイできる。そしてそれが罠だ、と記事の筆者は言う。
何が問題か、列挙すると長い:
express.json()にサイズ制限がないため、数百MBのリクエストを大量に送ればメモリを食い尽くすDoS攻撃が成立するcors()はcredentialsと組み合わせると、任意のオリジンからのクロスサイトリクエストが通ってしまう/fetch-coverは**SSRF(Server Side Request Forgery)**の典型例。http://169.254.169.254/latest/meta-data/iam/security-credentials/を渡せば、クラウドインスタンスの認証情報が漏洩する。Capital Oneの2019年の大規模情報漏洩もSSRFが起点だったreq.bodyを無検証でDBに渡しているため、__proto__を使ったプロトタイプ汚染攻撃が可能- レートリミットなし、405応答なし、エラー時にスタックトレースをクライアントに返す可能性あり
これらの問題は、どれもテストで検出されない。 テストはハッピーパスを通る。エージェントは「成功」と報告する。脆弱性は緑のチェックマークをつけて本番に出荷される。
「注意する」ではスケールしない
記事が引用しているのは、SupabaseとAikidoが発表したレポートの一節だ。
「AIに『動くようにしろ』と言うと、あなたを守っているセキュリティチェックを削除してしまうことがある」
人間も深夜2時、テストが赤でデプロイ時間が迫っているときに同じことをする。違いは、エージェントはそれを素早くやり、「なぜこのチェックがあったんだっけ」という小さな警戒心を持たないことだ。
だから解決策は「もっと注意する」ではない。フレームワークのデフォルトを安全にすることだ。「動かす」と「安全にする」が同じ操作になれば、AIが生成するコードもその恩恵を受ける。
筆者がこの哲学で構築しているフレームワークが**DaloyJS**だ(GitHubリポジトリ)。
「起動を拒否する」という設計
DaloyJSの最もユニークな設計思想は、危険な設定を検出したら起動自体を拒否することだ。
本番に出荷される脆弱性は高くつく。CIのpnpm startでクラッシュするのはタダだ。
1. ワイルドカードCORSの拒否
// 本番環境ではコンストラクタで例外を投げ、起動しない
app.use(cors({ origin: "*", credentials: true }));
// Error: cors({ origin: "*" }) refused in production: a wildcard CORS origin
// exposes every state-changing route cross-origin.
実際のオリジンを許可リストで指定しない限り、サーバーが起動しない。
2. 弱いシークレットの拒否
session({ secret: "changeme" }); // 既知のプレースホルダー
session({ secret: "short" }); // 最小バイト長未満
session({ secret: "aaaaaaaaaaaaaaaa" }); // 単一文字の繰り返し
// エラーメッセージが解決策まで示す:
// session(): production secret is too short (5 bytes; require >= 32).
// Generate one with `openssl rand -base64 48` and load it from an env var.
「あとで変える」は、セキュリティが死に行く場所だ、と筆者は言う。
3. 認証なしの状態変更エンドポイントの拒否
app.metrics();
// app.metrics() refused in production: provide opts.token to require auth.
app.metrics({ token: process.env.METRICS_TOKEN }); // これはOK
認証なしで状態を変えるパブリックルートは、本番では意図的な選択よりミスである可能性が高い、というのがフレームワークの立場だ。
4. プロキシ信頼設定の強制
ロードバランサー背後でX-Forwarded-Forを読んでIPレートリミットをかける場合、トラスト設定を明示しないとヘッダーを読めない。未設定のまま運用すれば、攻撃者は偽のX-Forwarded-ForでIP制限を回避できる。
SSRFを塞ぐfetchGuard()
AIエージェント時代、バックエンドは頻繁にアウトバウンドHTTPリクエストを発行する。ユーザーが貼ったURLを要約する、Webhookが顧客指定のURLを叩く、エージェントツールがページを取得する。いずれも攻撃者が内部を指すURLを渡せる場所だ。OWASPのSSRF解説が示す通り、この攻撃パターンはクラウド環境で特に影響が大きい。
DaloyJSはドロップイン代替として@daloyjs/coreのfetchGuard()を提供する。
import { fetchGuard, SsrfBlockedError } from "@daloyjs/core";
const safeFetch = fetchGuard();
handler: async ({ body }) => {
try {
const r = await safeFetch(body.url);
return { status: 200 as const, body: await r.arrayBuffer() };
} catch (e) {
if (e instanceof SsrfBlockedError) {
return { status: 400 as const, body: { error: "blocked target" } };
}
throw e;
}
}
デフォルトでブロックする対象:ループバック、RFC1918プライベートアドレス、リンクローカル、AWSの169.254.169.254をはじめとする各クラウドのメタデータエンドポイント、GCPのmetadata.google.internal、Alibaba・Oracle・キャリアグレードNATのレンジ、マルチキャスト、http/https以外のスキーム。
特に重要な点はリダイレクト追跡だ。ナイーブなSSRF対策は最初のURLしかチェックしないため、302 Location: http://169.254.169.254/...を返すパブリックURLで回避できる。fetchGuard()はリダイレクトを手動で追跡し、すべてのホップを再検証する。
記事の正直さについて
記事の最後で筆者は「セキュリティの記事で勝ちだけ並べるのはマーケティングで、3画面先から匂いがする」と述べ、DaloyJSが防げないものも正直に列挙している。SQLインジェクション(使うORMによる)、依存関係のサプライチェーン攻撃、ビジネスロジック上の認可ミスなどはフレームワーク層では防げない。
フレームワークのデフォルトが安全でも、コードを書くのは人間かエージェントであることに変わりはない。 デフォルト安全な設計はその一段階目にすぎない、というのが記事の結論だ。
詳細はYour AI shipped a backend that boots. That is the whole problem.を参照していただきたい。




