Codex のフルレビューを Claude が観点別に検証する中で出た、綴Photo モバイル版のセキュリティ寄りの2つの「思い込み」を、勉強のためにまとめたものです。
綴Photo のモバイル版は、撮った写真と綴り(気持ち)をローカルDBに保存していて、クラウド同期を少しずつ足している段階だ。同期に使う認証トークン(JWT)は、ログイン機能がまだ無いので、開発中は手で発行したものを --dart-define でビルド時に渡している。
レビューで指摘されたのは、その周辺に「見えてないだけで、実はそこにある/生きている」という思い込みが2つ重なっていたこと。1つはトークンの渡し方、もう1つは同期ボタンの隠し方。どちらも「見えない」を「無い・安全」と読み替えていた。まとめておく。
思い込み①: --dart-define のトークンは「環境変数だから隠せる」?
コードはこういう形だった。トークンとベースURLを String.fromEnvironment で受け取り、通信時にそのまま Authorization ヘッダへ載せます。
// ビルド時に --dart-define で渡す想定の設定
static const String jwtToken = String.fromEnvironment('JWT_TOKEN', defaultValue: ''); // ← ビルド時に値が確定(実行時には外から変えられない)=バイナリに焼き込まれる
static const String apiBaseUrl =
String.fromEnvironment('API_BASE_URL', defaultValue: 'http://localhost:8000'); // ← 平文HTTP・デフォルト値もバイナリに残る
// 通信時:トークンがあれば Bearer として付ける(送り先のスキームは見ていない)
if (token.isNotEmpty) headers['Authorization'] = 'Bearer $token'; // ← 焼き込まれたトークンを送り先に関係なくそのまま載せて送る引っかかるのは2つ。1つは、アプリのファイルを文字列検索すると焼き込まれた文字列がそのままで出てきます——特別なハッキングは要らず、手軽にできます。
もう1つは送り先。ベースURLのデフォルトが平文の http://、しかもスキーム(http か https か)も送り先がローカルかどうかも見ずに Bearer を付けています。誤って非ローカルの http:// を指したまま実トークンを渡すと、トークンが平文で飛ぶ=同じネットワークにいる第三者に盗み見られ得ます。
思い込み②: kDebugMode で隠したボタンは「無い」?
同期と画像アップロードのボタンは、設定画面で kDebugMode で囲って、本番では出ないようにしてあった。
UIをどこに置くかは設計上の壁ではありません。将来「同期を本番導線に昇格させよう」としたとき、思い込み①(平文にトークンを載せる)が一緒に表に出てきます——②のUI壁が外れた瞬間に、焼き込みトークン(①)と平文HTTP黙認が同時に効いてきます。個別には「まだ大丈夫」に見えていた偶然の壁が、1枚剥がれると残りも連鎖します。
直した方向
いま実装したのは、送り先のガードだ。平文HTTP かつ送り先が非ローカルのときは、トークンを載せようとした時点で例外を投げて止めます(フェイルクローズ=迷ったら送らない側に倒します)。https と、ローカル開発の接続先(localhost など)は通します。トークンが空のとき(守る秘密が無い)は素通り。
// 平文HTTP × 非ローカル宛にトークンを送ろうとしたら弾く(送ってから後悔できないので手前で止める)
if (token.isEmpty) return; // 守る秘密が無い
if (uri.scheme == 'https') return; // 暗号化されている
if (localHosts.contains(uri.host)) return; // ローカル開発接続は許可
throw StateError('平文で非ローカルへトークンを送ろうとした → 拒否');リリースビルドにはトークンを渡さない運用は、方針としてコメントに残し、実装はログイン機能を作るときに回した。
kDebugMode の方は、ゲートの意図をコメントで明文化した。「これは本番からクラウド送信に到達させない設計上の壁であって、単なるUI配置ではない。外す前に上のガードと保管庫移行を固めること」と書いて、偶然の壁を意図のある壁に格上げしておいた。
学びポイント
--dart-defineはコンパイル時の定数で、バイナリに文字列として焼き込まれます。サーバの環境変数の「実行環境に隠せる」感覚を持ち込むと裏切られます。モバイルのクライアントに本物の秘密は置けない前提で考えます- 「画面に出していない(
kDebugMode)」は「機能が無い」ではありません。出口のコードが生きているなら、UIの非表示は設計上のガードではなく、いつでも剥がれる偶然の壁 - 平文HTTP × 非ローカルへトークンを送らない、はフェイルクローズ(条件を満たさないなら送らない・弾く)で書きます。
httpsとローカルだけ通します。判定に迷う・想定外の入力が来たとき「通す」と漏れは静かに成功して気づけません。だからデフォルト拒否にして、通すのは明示的に安全だと分かったケースだけにします - 「見えない」状態(バイナリの中・UIの裏)を「無い・安全」と読み替えません。見えないだけでそこにあります
おわりに
2つの指摘は別の話に見えて、根っこは同じ「見えない=無い」という錯覚だった。バイナリの中に焼き込まれたトークンも、UIの裏に隠れた送信コードも、画面や普段の開発からは見えません。でも見えないことと存在しないことは違います。
レビューがここを突けたのは、「このトークンはどこに焼かれて、どの送り先まで届き得るか」「このボタンの先のコードは本番に残るのか」と、見えている表面の一枚下まで追ったからだった。表面上は「本番には出ない開発用ボタン」で片付くものを、出口まで辿って初めて「壁になっていない」と分かります。
この記事は2026年6月時点の自分の理解で書いています。仕様や認識が変わってる可能性があります。