個人で作ったWindowsアプリを有料で配布しています。認証ファイルを読むツールなので、改めて安全性を確認しました。
その結果、想定していた場所には問題がなく、想定していなかった場所で3つ問題が出ました。 特にコード署名については日本語の情報がほとんど見つからず、調べ直して初めて分かったことがあったので記録します。
対象は自作の常駐ツールですが、内容はPyInstallerや.NETでWindowsアプリを配る人全般に当てはまります。
前提:何を作っているか
Claude Code と Codex の残り使用量をタスクトレイに表示する、Windows用の常駐ツールです。
動作としては、ローカルの認証ファイル(~/.claude/.credentials.json と ~/.codex/auth.json)からアクセストークンを読み、各サービスの使用量エンドポイントに問い合わせて残量を表示します。
つまり「利用者の認証情報を読んで、ネットワークに出る」タイプのアプリです。 ここが今回の確認の出発点でした。
1. 配布バイナリに開発PCの情報が残っていないか
最初に確認したのはここです。結果は問題なしでしたが、確認方法は書いておく価値があります。
配布物に混入しがちなのは次のものです。
-
C:\Users\<開発者名>\...のフルパス - Windowsユーザー名、ホスト名
- PDB(デバッグ情報)への参照パス
特にPDB参照は厄介で、Releaseビルドでも既定では埋め込まれます。 .NETの場合はプロジェクト側で明示的に無効化します。
<PropertyGroup Condition="'$(Configuration)'=='Release'">
<DebugType>none</DebugType>
<DebugSymbols>false</DebugSymbols>
</PropertyGroup>
確認はバイナリを直接検索します。テキストファイルだけでなく、EXEとDLLの両方を対象にするのが重要です。
grep -ac "あなたのユーザー名" MyApp.exe
grep -aoE "C:\\\\Users\\\\[A-Za-z0-9_]+" MyApp.exe | sort -u
grep -aoE "[A-Za-z0-9_.]+\.pdb" MyApp.exe | sort -u
-a はバイナリをテキストとして扱うオプションです。これがないとgrepが「Binary file matches」としか言わず、中身が見えません。
2. ログに利用者の情報を書いていた
ここで実際に問題が見つかりました。
このツールはEXEと同じフォルダーに動作ログを出します。トークンの値は最初から出力しない設計にしていたので、そこは問題ありませんでした。問題はパスの方でした。
[2026-06-02 09:07:34] [Claude] ファイル読み込み: C:\Users\taro\.claude\.credentials.json
このログは、買った人のPCでは買った人の名前が入ります。 実際のログを調べたら、641行中112箇所にフルパスが残っていました。
不具合の問い合わせでログを送ってもらう運用を想定していたので、そのたびに相手のWindowsユーザー名を受け取ることになります。 受け取りたくない情報です。
修正はホームディレクトリを ~ に置換するだけです。
from pathlib import Path
def _mask_path(p) -> str:
"""ホームディレクトリを ~ に置換してログのパスを匿名化する"""
try:
return "~/" + str(Path(p).relative_to(Path.home())).replace("\\", "/")
except ValueError:
# ホーム配下でないパスはファイル名だけ残す
return Path(p).name
修正前: C:\Users\taro\.claude\.credentials.json
修正後: ~/.claude/.credentials.json
ValueError 側の分岐が地味に重要で、別ドライブや共有フォルダーを指定された場合にフルパスがそのまま出るのを防ぎます。
この問題に気づきにくいのは、開発者のPCではフルパスに出るのが自分の名前だからです。ログを見ても違和感がありません。他人のPCで動くことを想像しないと気づけない種類の見落としでした。
3. PyInstaller製のEXEは、grepでは中身を判定できない
今回いちばんハマったのはここです。しかも一度、間違った結論を出しました。
修正版をビルドしたあと、「今売っているEXEに修正が入っているか」を確認しようとしました。やったことは単純で、関数名でgrepしただけです。
grep -ac "_mask_path" TokenChecker.exe
# → 0
0件だったので「販売中の版には修正が入っていない」と判断しました。これが間違いでした。
念のため、修正済みソースからビルドしたばかりのEXEに同じ検索をかけたところ、こちらも0件でした。
grep -ac "_mask_path" dist/TokenChecker.exe # 確実に入っているはず
# → 0
理由は考えれば当たり前で、PyInstallerはPythonコードをバイトコード化して圧縮アーカイブに格納するため、平文の関数名はバイナリ中に存在しません。この検索方法は最初から何も判定していませんでした。
つまり 「0件」は「入っていない」ではなく「この方法では分からない」 でした。両者はまったく違います。
教訓:陽性対照を取る
「Aには入っていない」と主張するなら、同じ方法で「Bには入っている」ことを示せるか先に確かめる。
今回なら、確実に含まれる新ビルドで同じ検索をかけるだけで、方法が壊れていると即座に分かりました。実験の基本ですが、コードの確認作業でも同じことが必要でした。
正しい確認方法は次のどちらかです。
- 実際に動かしてログを見る(今回の目的にはこれが最短)
- ソース側で関数を直接テストする
# 関数の挙動そのものを確かめる
for p in [r'C:\Users\test\.claude\.credentials.json', r'D:\other\auth.json']:
print(p, '->', _mask_path(p))
# C:\Users\test\.claude\.credentials.json -> ~/.claude/.credentials.json
# D:\other\auth.json -> auth.json
なお結論自体(旧版がフルパスを記録していたこと)は、実際のログに112件残っていたという別の証拠で裏付けられていました。結論は合っていましたが、最初に挙げた根拠は誤りでした。
4. コード署名:日本の個人は、最安の選択肢が使えない
ここがいちばん情報を探すのに苦労しました。
未署名のEXEを配ると、初回実行時にSmartScreenの警告が出ます。「WindowsによってPCが保護されました」というあれです。ウイルス検出ではなく署名がないことによる表示ですが、買った人が起動できなければ売れていないのと同じなので無視できません。
調べた選択肢は次のとおりです(2026年8月時点)。
| 選択肢 | 費用 | 日本の個人が使えるか |
|---|---|---|
| Azure Trusted Signing | $9.99/月 | 使えない |
| Sectigo Japan(OV) | 55,000円/年(税別) | 使える(個人・個人事業主可) |
| DigiCert | 97,900円/年 | 使える |
| 自己署名証明書 | 無料 | 意味がない(後述) |
最安のAzure Trusted Signingが、日本在住の個人には使えません。
個人向けの本人確認に対応しているのは米国・カナダ在住者のみです(組織であればEU・UKも対象)。月$9.99という価格に加え、署名直後からSmartScreenに引っかからない点が優れているのですが、日本の個人開発者は現時点で対象外でした。ここが分かるまで一番時間を使いました。
結果として、日本の個人にとっての実質的な下限は年5.5万円になります。
自己署名証明書では解決しない
「自分で証明書を作れば無料では」と考えましたが、配布用途では機能しません。
Windowsは「信頼されたルート認証局」から辿れる証明書しか信用しません。自己署名証明書はその連鎖の外側にあるため、SmartScreenの警告も、ウイルス対策ソフトの誤検知も、発行元不明の表示も、何ひとつ改善しません。
署名に意味があるのは「第三者が身元を保証している」からで、自分で自分を保証しても情報量がゼロだからです。自己署名が有効なのは、各PCに証明書を事前インストールできる社内配布のような場面に限られます。
今回の判断:買わない
年5.5万円は、その商品の年間売上が5.5万円を超えてから検討すべきコストです。今回は見送りました。
代わりに無料でできる対策を取りました。
- ソースコードの公開 — 後述しますが、これが一番効きます
- SHA-256ハッシュの公開 — 配布物に同梱し、商品説明にも記載
- Microsoftへの誤検知申告 — 検体を提出して除外を依頼できます
- SmartScreen警告の回避手順を説明書に明記 — 「詳細情報→実行」を先回りして案内する
2については、配布ZIPに SHA256.txt を入れ、確認方法も書きました。
Get-FileHash .\TokenChecker.exe -Algorithm SHA256
認証情報を読むツールは、ソースを公開しないと売れない
最後に、確認作業を通して考えが変わった点です。
コードを読み直した結果、実装上は問題ありませんでした。トークンはログにも出さず、ディスクにも保存せず、送信先も公式の2エンドポイントだけです。
ただ、それは私が中を見られるから言えることです。
買う側から見ると、状況はこうなります。
ソースが見えないEXEが
↓
自分の認証ファイルを読み
↓
アクセストークンを取り出して
↓
外部へ送信する
実装が正しいことと、買う人がそれを確認できることは別問題でした。 そして後者がないと、警戒心のある人ほど買いません。認証情報を扱うツールなら、なおさらです。
なのでソースを公開することにしました。 隠していると「怪しいEXE」ですが、公開すれば「検証できるコード」になります。トークンを出力しない実装になっていることも、送信先が2つだけであることも、読めば分かる状態にする方が早い。
ビルド済みEXEの販売は続けます。買う人が払っているのは「コードを読む権利」ではなく「Pythonを入れてPyInstallerでビルドする手間を省けること」 なので、公開しても商品は成立すると判断しました。
まとめ
自作アプリを有料配布する前に確認したこと、実際に出た問題です。
-
バイナリに開発PCの情報が残っていないか — .NETは
DebugType=noneを明示する。grep -aでEXEとDLLの両方を見る -
ログに利用者の情報を書いていないか — 自分のPCでは自分の名前が出るので気づきにくい。ホームディレクトリは
~に置換する - 確認方法そのものが有効か — PyInstaller製EXEはgrepで中身を判定できない。「Aに無い」と言う前に、同じ方法で「Bに有る」ことを示せるか確かめる
- コード署名は費用対効果で判断する — 日本の個人は最安のAzure Trusted Signingが使えず、実質年5.5万円から。自己署名は配布用途では機能しない
- 認証情報を読むなら、ソース公開が実質的に必須 — 実装が正しいことと、買う人が確認できることは別
一番の収穫は3つ目でした。確認したつもりで何も確認していなかったというのが、今回いちばん危なかった点です。
この記事で扱ったツールのソースコードと、ビルド済み版はこちらです。
- ソースコード: https://github.com/Creator-HA/token-checker
- ビルド済み版(BOOTH・300円): https://smallworkslab.booth.pm/items/8444958?utm_source=qiita&utm_medium=article&utm_campaign=security_check_20260818_tokenchecker
ビルド済み版は、Pythonの環境構築なしにそのまま使いたい方向けです。中身は同じものなので、自分でビルドする方はリポジトリだけで完結します。