AIで作ったデジタル商品のZIPを販売前に検査する|混入2件を止めた

販売用の ZIP を開くと、商品ファイルの横に.envと生成メモが入っていました。AI で中身を作る速度は上がっても、作業フォルダを丸ごと圧縮すると、渡すつもりのないファイルまで同じ速さで梱包されます。
2026 年 9 月 29 日、AI 提案書テンプレートを想定した小さな実験をしました。最初の ZIP は 4 ファイル中 2 ファイルが販売対象外でした。対象外の名前を除外する方法ではなく、販売してよい 2 ファイルだけを指定して作り直すと、検査はFAILからPASSへ変わりました。
私は、デジタル商品では「フォルダを ZIP にできた」を完成にしません。購入者へ渡すファイル名を先に決め、その一覧と一致した時だけ販売候補へ進めます。
前回完了時から変えたこと
前回は、AI で作った PDF 商品のページ数、用紙サイズ、空ページなどを販売前に確認しました。今回は PDF の外側にある配送パッケージを扱います。本文が正しくても、ZIP へ内部メモや環境設定が混ざれば納品物として失敗です。
| 時点 | 検査対象 | 見落とす問題 |
|---|---|---|
| 前回完了時 | PDFの構造と表示 | ZIPへ混ざる作業ファイルは見ません |
| 今回完了後 | ZIP内の全ファイル名 | 商品内容の価値は人が確認します |
読み飛ばせる章: Pythonの
zipfileを使ったことがある場合は、「除外ではなく許可リストで作り直す」から進めます。
なぜ、圧縮する前に渡す物を決めるのか
AI へ資料やテンプレートを作らせると、完成品のほかにプロンプト、比較メモ、一時出力が残ります。API を使った処理なら、作業用の.envが同じ場所にあることもあります。そこで親フォルダをそのまま ZIP へ入れると、作業場所と販売物の境界が消えます。
今回の実験では本物の API キーを使っていません。.envにはDEMO_TOKEN_DO_NOT_USEという無効な文字列だけを置きました。それでも、販売物へ環境設定ファイルが入った状態は不合格にします。本物かどうかを ZIP 検査へ判断させるより、.envという名前自体を販売対象から外す方が単純だからです。
[AIで商品素材を作る]
│
▼
[作業フォルダ]
├── README.txt 販売する
├── template.md 販売する
├── prompt-notes.md 内部メモ
└── .env 環境設定
│
▼
[許可した2ファイルだけでZIPを作る]
│
▼
[ZIP内の名前を再検査]
├── PASS → 人が内容を確認
└── FAIL → 販売候補へ進めない
GitHub の公式文書も、秘密情報をコードへ直接書かず、環境変数やシークレット管理サービスを使う方法を案内しています。漏えい後の履歴修正には副作用があるため、配布前に止める方が負担は小さくなります。
用語をそろえる
| 用語 | この回での意味 |
|---|---|
| ZIP | 複数の販売ファイルを1つへまとめるアーカイブ形式です |
| 作業フォルダ | 完成品、生成メモ、設定ファイルが同居する制作中の場所です |
| 許可リスト | 販売物へ入れてよいファイル名だけを明示した一覧です |
| 除外リスト | 入れてはいけないファイル名を並べた一覧です |
| アーカイブ内名 | ZIPを展開した時に現れるファイルのパスです |
| 成果物ゲート | 必要条件がそろわない限り、販売候補へ進めない検査です |
4ファイルを丸ごと圧縮すると2件混ざった
実験では、販売するREADME.txtとtemplate.mdに加え、内部用のprompt-notes.mdと.envを作りました。次のスクリプトは、混入した ZIP と修正後の ZIP を続けて作ります。
from pathlib import Path
from zipfile import ZIP_DEFLATED, ZipFile
root = Path("product-lab")
src = root / "product-src"
src.mkdir(parents=True, exist_ok=True)
files = {
"README.txt": "AI提案書テンプレート v1.0\n",
"template.md": "# 提案書\n\n顧客の課題:\n",
"prompt-notes.md": "生成時の試行錯誤メモ\n",
".env": "OPENROUTER_API_KEY=DEMO_TOKEN_DO_NOT_USE\n",
}
for name, body in files.items():
(src / name).write_text(body, encoding="utf-8")
blocked = {".env", "prompt-notes.md"}
def inspect(path: Path) -> None:
with ZipFile(path) as archive:
names = archive.namelist()
hits = [name for name in names if Path(name).name in blocked]
state = "FAIL" if hits else "PASS"
print(f"{path.name}: GATE={state} files={len(names)} blocked={len(hits)}")
for name in names:
print(f" {name}")
before = root / "product-before.zip"
with ZipFile(before, "w", ZIP_DEFLATED) as archive:
for path in sorted(src.iterdir()):
archive.write(path, arcname=f"product-src/{path.name}")
inspect(before)
ready = root / "product-ready.zip"
with ZipFile(ready, "w", ZIP_DEFLATED) as archive:
for name in ("README.txt", "template.md"):
archive.write(src / name, arcname=name)
inspect(ready)
このコードをzip-gate-lab.pyとして保存し、空の作業場所で実行しました。
python3 zip-gate-lab.py
実行結果は次の通りです。
product-before.zip: GATE=FAIL files=4 blocked=2
product-src/.env
product-src/README.txt
product-src/prompt-notes.md
product-src/template.md
product-ready.zip: GATE=PASS files=2 blocked=0
README.txt
template.md
最初の ZIP は、4 ファイルを問題なく圧縮できています。しかし、販売物としては 2 件の混入があるため失敗です。圧縮コマンドの終了コードだけでは、この違いを判定できません。
除外ではなく許可リストで作り直す
.envとprompt-notes.mdだけを除外しても、次にcustomer-draft.csvやraw-output.jsonが増えれば検査をすり抜けます。作業ファイルの名前は増えます。販売するファイルは、今回なら 2 件で固定できます。
修正後の処理は次の 2 行を許可リストとして使います。
for name in ("README.txt", "template.md"):
archive.write(src / name, arcname=name)
この形なら、AI が作業フォルダへ新しいメモを増やしても ZIP には入りません。販売ファイルを追加する時は、一覧を人が変更します。私は、自動生成へ自由を持たせる場所と、購入者へ渡す境界をここで分けます。
作成後の再検査も省きません。コードの意図ではなく、実際の ZIP からnamelist()で全件を読みます。Python のzipfileは ZIP の作成、読み取り、書き込み、一覧取得を扱えます。別の外部パッケージを足さず、同じ標準ライブラリで作成と検査ができます。
名前検査だけで秘密を探し切ろうとしない
今回のゲートは、既知の危険な名前を 2 件止めました。ファイル内容に貼り付けたトークンや、名前を変えた顧客データまでは判定しません。
OWASP は、API キーや認証情報が平文のソースコードや設定ファイルへ残る問題を挙げています。また、人が秘密情報へ触れる回数を減らし、保管やローテーションを自動化する考え方を示しています。販売用 ZIP の名前検査は、その代わりではありません。
実運用では役割を分けます。
| 段階 | 見るもの | 止める例 |
|---|---|---|
| 制作中 | シークレット管理と権限 | APIキーを作業ファイルへ直書きする操作です |
| 梱包時 | 許可したファイル名との一致 | .envや生成メモの混入です |
| 販売前 | 人による本文確認 | 顧客名、誤情報、利用条件の欠落です |
もし本物の認証情報を公開した場合、ZIP を削除するだけでは終わりません。GitHub は、パスワードやトークンなら最初に失効またはローテーションするよう案内しています。履歴や複製へ残る可能性があるためです。
よくあるエラー
| 症状 | 原因 | 対応 |
|---|---|---|
| ZIP作成は成功したのにゲートが落ちます | 圧縮処理と販売条件を同じ成功として扱っています | ZIP内名を読み、許可リストと比較します |
| 新しいメモが販売物へ混ざります | 危険な名前だけを除外しています | 入れてよいファイル名だけでZIPを作ります |
.envを消したので安全だと判断しました | 別ファイルの本文へ秘密が残る可能性があります | 内容検査とシークレット管理を別に行います |
| 修正後も古いZIPが配布されます | 出力名が同じで、販売画面の添付を更新していません | 作成した版と添付した版の一致を確認します |
| 購入者がZIPを展開しにくいです | ZIP内へ作業フォルダ名まで入れています | arcnameで購入者向けの短い名前にします |
| 検査へ未知のファイルが出ません | 作成前のフォルダだけを見ています | 完成したZIPを開き直して全件を読みます |
今回の失敗を残す
最初のFAIL files=4 blocked=2は、捨てるべき結果ではありません。作業フォルダを丸ごと圧縮する実装では、商品ファイルと内部ファイルを分けられないと分かりました。修正後はPASS files=2 blocked=0です。変えたのは AI のプロンプトではなく、梱包するファイルの選び方でした。
この検査に合格しても、商品が売れるとは言えません。提案書テンプレートが購入者の仕事を短くするか、説明が足りるかは別の確認です。それでも、内部メモと環境設定を購入者へ渡さない条件は機械で固定できます。
AI でデジタル商品を増やすほど、作業フォルダの中身も増えます。私は「全部入れてから危険物を引く」方法をやめ、販売すると決めた物だけを ZIP へ入れます。地味ですが、生成速度が上がった時ほど効く境界です。
一次情報
| 資料 | 確認した内容 |
|---|---|
| Python 3 Documentation: zipfile | ZIPの作成、読み取り、書き込み、一覧取得を標準ライブラリで扱う仕様 |
| OWASP Secrets Management Cheat Sheet | APIキーなどの秘密情報を平文で散在させず、保管と運用を管理する考え方 |
| GitHub Docs: Removing sensitive data from a repository | 漏えいした認証情報の失効やローテーション、履歴修正の副作用、再発防止策 |


