More → Code を開くと、ファイルツリーの下に src/ や components/ がずらりと並びます。どこから読めばいいのか分からないまま、スクロールだけして閉じた経験が、私にもあります。
Rork のドキュメントは、鍵は Secrets に置き、コードにも会話にも貼らないようにと案内しています。その約束が守られているか、つまりコードの中に鍵が実際に入っていないかを、私たちは普段なかなか確かめられません。
最初にお伝えしたいのは、全ファイルを読む必要はない、ということです。鍵には「形」と「名前」の癖があります。そこだけを機械に探させれば、読む場所は数行に絞れます。ここからは、そのための小さなスクリプトと、見つかったときの手順を書き残します。確かめる手段がないまま「たぶん大丈夫」と思ってしまうのは、よくあることなのかもしれません。
探す前に、コードを手元に出す方法を決めます
Rork の Code タブはそのまま読めます。無料プランでは「Read-only」の表示が出て編集はできませんが、全ファイルの閲覧はできる、とドキュメントにあります。目で探すだけなら、ここで足ります。
スクリプトで探すなら、GitHub 連携でコードを手元に置くのが近道です。ドキュメントによれば、連携するとコードは通常のリポジトリになり、エージェントの1ターンにつき1コミットで積み上がります。clone して、そのフォルダにスクリプトを向ければ準備は終わりです。
ひとつ、安心材料があります。Secrets に入れた値は、コードとは別にプロジェクトに保管され、コミットには含まれないとドキュメントの FAQ に書かれています。逆に言えば、GitHub 側のコードに鍵が見つかったなら、それは Secrets を通らずにコードへ書かれた鍵です。
鍵が紛れ込むときの目印は、3つに分けられます
私が探しているのは、次の3種類です。
| 目印 | 探し方 | 見つかる例 |
|---|---|---|
| 鍵そのものの形 | プロバイダごとの接頭辞(sk-、sk_live_、AIza、ghp_ など) | OpenAI 互換、Stripe 秘密鍵、GitHub トークン |
| 名前が秘密らしい変数への直代入 | SECRET・TOKEN・API_KEY を含む名前に、16 文字以上の文字列を直接代入している行 | 天気 API のキーを定数に書いた行 |
EXPO_PUBLIC_ を付けた秘密 | EXPO_PUBLIC_ のあとに SECRET・SERVICE_ROLE などが続く名前 | Supabase の service role キーを公開名で読んでいる行 |
3つ目は、ドキュメントの警告に直結します。EXPO_PUBLIC_ で始まる変数はアプリ本体に送られ、それ以外はサーバー側に留まる、というのが Rork の決まりです。秘密の鍵にこの接頭辞を付けた時点で、アプリを入れた人なら誰でも取り出せる場所に置いたことになります。
スクリプト本体です
Python 3 の標準ライブラリだけで動きます。拡張子は .ts、.tsx、.js、.swift、.kt、.plist などを見ます。
#!/usr/bin/env python3
"""GitHub に書き出した Rork のコードから、鍵の直書きを探す。"""
import re, sys, pathlib
SKIP_DIRS = {"node_modules", ".git", "Pods", "build", "dist", ".expo"}
EXTS = {".ts", ".tsx", ".js", ".jsx", ".json", ".swift", ".kt", ".env", ".plist", ".md"}
# 1) 鍵そのものの「形」
SHAPES = {
"OpenAI/互換 sk-": re.compile(r"\bsk-[A-Za-z0-9_-]{20,}"),
"Stripe 秘密鍵": re.compile(r"\b[sr]k_(live|test)_[A-Za-z0-9]{16,}"),
"Google API キー": re.compile(r"\bAIza[0-9A-Za-z_-]{35}\b"),
"GitHub トークン": re.compile(r"\bgh[pousr]_[A-Za-z0-9]{36,}"),
"Resend": re.compile(r"\bre_[A-Za-z0-9]{20,}"),
}
# 2) 秘密っぽい名前に、長い文字列を直接代入している
ASSIGN = re.compile(
r"""(?i)\b([A-Z0-9_]*(SECRET|TOKEN|PRIVATE|SERVICE_ROLE|API_?KEY)[A-Z0-9_]*)\b\s*[:=]\s*["']([^"'\s]{16,})["']"""
)
# 3) 公開される接頭辞に、秘密っぽい名前を付けている
PUBLIC_SECRET = re.compile(
r"(?i)\bEXPO_PUBLIC_[A-Z0-9_]*(SECRET|PRIVATE|SERVICE_ROLE|TOKEN)[A-Z0-9_]*"
)
def walk(root):
for p in root.rglob("*"):
if p.is_file() and p.suffix in EXTS and not any(s in p.parts for s in SKIP_DIRS):
yield p
hits = 0
for path in walk(pathlib.Path(sys.argv[1] if len(sys.argv) > 1 else ".")):
for n, line in enumerate(path.read_text(errors="ignore").splitlines(), 1):
found = [name for name, rx in SHAPES.items() if rx.search(line)]
if ASSIGN.search(line) and "process.env" not in line:
found.append("秘密っぽい名前に値を直書き")
if PUBLIC_SECRET.search(line):
found.append("EXPO_PUBLIC_ に秘密っぽい名前")
for f in found:
hits += 1
print(f"{path}:{n}: [{f}]") # 行の中身は出さない
print(f"-- {hits} 件")
sys.exit(1 if hits else 0)print で出すのは、ファイル名・行番号・種類だけです。行の中身は意図して出していません。画面共有や、ログを Rork のチャットに貼る場面で、鍵そのものを二次的に漏らしたくないからです。
使い方は clone したフォルダで次のように実行するだけです。
python3 scan_keys.py ./my-rork-project手元のサンプルで動かした結果です
本物の鍵は使えませんので、形だけ似せた偽の値で小さなプロジェクトを作り、動かしました。天気 API のキーを定数に書いたファイル、apiKey に sk- 形式の文字列を入れたファイル、EXPO_PUBLIC_SERVICE_ROLE_KEY を読むファイル、Firebase の設定 JSON、そして問題のないファイルを一つずつ用意しています。
出力はこうなりました。
demo/src/weather.ts:1: [秘密っぽい名前に値を直書き]
demo/src/ai.ts:1: [OpenAI/互換 sk-]
demo/src/ai.ts:1: [秘密っぽい名前に値を直書き]
demo/src/ai.ts:3: [EXPO_PUBLIC_ に秘密っぽい名前]
demo/config/firebase.json:1: [Google API キー]
-- 5 件読み取れることが三つあります。まず、process.env.RESEND_API_KEY のように環境変数を読んでいるだけの行と、TOKEN_REFRESH_SECONDS = 3600 のような設定値は、どちらも引っかかりませんでした。次に、ai.ts の1行目は2種類の目印に同時に当たっています。同じ行が複数回数えられるのは、仕様です。そして最後が、いちばん大事な点です。
Firebase の設定ファイルに出る警告は、慌てなくて大丈夫です
上の結果の最後、firebase.json の1件は、本物の漏えいではありません。Firebase の設定に入っている API キーは、Google のドキュメントでも一般的な秘密鍵とは性質が違うものとして説明されています。アプリに同梱されて配られる前提の識別子で、守りはキー自体ではなく、Firebase 側のセキュリティルールや制限設定にあります。
個人開発で運用している私の iOS アプリでも、GoogleService-Info.plist に API キーの欄があります。スクリプトに .plist を含めたのは網羅のためですが、そこで当たる分は、誤検知として読み飛ばしています。
要するに、スクリプトの出力は「疑う順番の一覧」であって、「漏えいの確定通知」ではありません。1件ずつ、次の問いで仕分けます。
- その値は、アプリを入れた人に見えても困らないものか(公開前提の識別子か)
- 見えると困るなら、誰が使うと請求や被害が起きるか
- いま使える鍵か、すでに失効した鍵か
本物が見つかったときは、順番がすべてです
ここで間違えやすいのが、Rork の画面でコードの鍵を消して終わりにしてしまうことです。ドキュメントには、鍵が公開された場所に出たときの手順が4段階で書かれています。
- 提供元のダッシュボードで、その鍵を失効またはローテーションします。実際に鍵を使えなくするのは、ここだけです。
- 新しい鍵を作って、値を控えます。
- Rork の Secrets で、同じ変数名に新しい値を入れて Save します。
- コードに鍵が書かれていた場合は、Rork に頼んで鍵を取り除き、変数から読むように直してもらいます。
順番を逆にして、先にコードだけ直すと、GitHub の履歴にはもとの鍵が残ったままです。履歴に残った値は、コードを直しても使えます。止めるのは、必ず提供元での失効が先です。
直すときの依頼文は、場所と置き場所をはっきり書くと通りやすくなります。
src/ai.ts の1行目に API キーが直接書かれています。
その行を削除して、サーバー関数から OPENAI_API_KEY を読む形に直してください。
EXPO_PUBLIC_ の名前は使わないでください。鍵をどこに置くのが正しいかは、環境変数と Edge Function の使い分けとして別にまとめています。置き場所の線引きはこちらの記事をご覧ください(そのAPIキー、アプリの中に入っていませんか)。
今日の一手
置き場所は決めたら終わりではなく、確かめて初めて決まります。 公開の前と、大きな機能を足した日の終わりに、このスクリプトを一度走らせます。それだけを、まず習慣にしていただければと思います。
件数がゼロでも、それは「目印にかからなかった」というだけで、安全の保証ではありません。それでも、数十ファイルを目で追うより、はるかに少ない手間で、いちばん多い失敗を先に拾えます。
鍵をサーバー側に置いたあと、アプリからの申告を信じすぎないことも、同じくらい大切です。リワード広告の報酬を端末からの合図だけで配らない設計は、有料記事にまとめています(リワード広告の報酬を、端末からの合図だけで配らない)。