自作のWebアプリを外部公開したとき、書き込みは守っていたのに、閲覧は無防備でした。URLさえ知られれば、帳簿も内部データも読める状態です。
原因は認証の実装ではなく、「何を守るか」の指定方法でした。
最初の実装: 隠したいものを列挙する
こう書いていました。
const PRIVATE_FILES = new Set(["data.json", "admin.html"]);
if (PRIVATE_FILES.has(file) && !isAuthorized(req)) {
return deny(403);
}
// それ以外は誰でも読める
一見わかりやすいコードです。守るべきものを明示していて、意図も伝わります。
問題は運用が始まってからでした。
- ログ出力を追加した →
access.logを列挙し忘れる - バックアップ機能をつけた →
backup-2026-08-06.jsonが読める - 設定を外だしした →
config.jsonが読める - 一時ファイルが残った →
data.json.tmpが読める
**ファイルが増えるたびに、列挙し忘れる可能性が増えます。**しかも忘れても何も起きません。エラーも警告も出ず、静かに公開されるだけです。気づくのは、誰かに読まれた後です。
変更後: 公開したいものを列挙する
逆にしました。
// 公開してよいものだけを列挙する。それ以外は自動的に非公開。
const PUBLIC_FILES = new Set([
"index.html",
"style.css",
"logo.png"
]);
if (!isLocal(req) && !PUBLIC_FILES.has(file)) {
// 合言葉を検証し、なければ拒否
const auth = authorize(req, url);
if (!auth.ok) return deny(auth.code);
}
違いは忘れたときに何が起きるかです。
| 方式 | 列挙を忘れると |
|---|---|
| 隠したいものを列挙 | 公開される(気づかない) |
| 公開したいものを列挙 | 非公開になる(すぐ気づく) |
後者は、忘れると「見えるはずのページが見えない」という形ですぐ表面化します。開発中に必ず気づくので、事故として外に出ません。
安全側に倒れる設計、という言い方もできます。
実際にやってみて分かったこと
1. 最初は面倒に感じる
新しいページを追加するたびに PUBLIC_FILES へ足す必要があります。忘れると自分が見られません。
ただこれは1回あたり数秒の手間で、しかも「見えない」とすぐ分かるので迷いません。列挙し忘れて情報が漏れる可能性と比べれば、比較になりません。
2. 一覧が「公開しているものの目録」になる
副次的な効果として、PUBLIC_FILES を見れば外部に何を出しているかが一目で分かります。
隠す方式だと「今、外から何が見えるのか」を知るには、ディレクトリ全体から非公開リストを引き算する必要があります。ファイルが増えるほど把握できなくなります。
3. 画像や配布ファイルも明示的になる
const PUBLIC_FILES = new Set([
"index.html",
"article-01.html",
"downloads/tool.js", // 配布物も明示
"ogp.png"
]);
「この配布ファイルは意図的に公開している」ことがコード上で読み取れます。レビューでも確認しやすくなりました。
確認方法
変更後は、非公開のはずのファイルが本当に取れないかを外から確認します。
# 公開しているもの
curl -o /dev/null -s -w "%{http_code}\n" https://<公開URL>/index.html
# 200
# 列挙していないもの
curl -o /dev/null -s -w "%{http_code}\n" https://<公開URL>/data.json
curl -o /dev/null -s -w "%{http_code}\n" https://<公開URL>/config.json
# 403 403
なお、外部公開の構成では接続元の判定そのものにも落とし穴があります。トンネル経由だと外部からのアクセスが自分のPCからの操作に見えるため、isLocal() が常に true を返して認証ごとすり抜けることがあります。その検査手順は別途まとめています。この記事の方式を入れても、そちらが抜けていると意味がありません。
まとめ
- 「隠したいものを列挙」は、忘れたときに公開される。忘れても気づけない
- 「公開したいものを列挙」は、忘れたときに非公開になる。すぐ気づく
- 手間はほぼ同じで、失敗したときの結果だけが違う
同じ構造は、権限設定やAPIの公開範囲にもあります。既定値を「許可」にするか「拒否」にするかという選択で、忘れたときの結末が変わります。