ChatGPTやClaude Codeに価格監視スクリプトを書かせたら requests.exceptions.SSLError が出た。直してと頼むと、AIは verify=False と urllib3.disable_warnings() を足し、エラーも警告も消えて「動きました」と返してくる。しかしこれは証明書の検証を外して、異常を見えなくしただけです。この記事では、verify=False を使わずに truststore.inject_into_ssl() と certifi の更新で直させる指示文を実物で載せます。あわせて、cronやsystemdで毎日動かしても、証明書の更新で止まらない状態にする手順まで示します。

ChatGPT・Claude Codeが出す verify=False+urllib3.disable_warnings() のどこが危ないか

最初の指示と、AIが出してきたコード

コードを書き慣れていない人が最初に出す指示は、だいたい次のような形になります。

example.com の商品ページ2つから価格を取ってきて、
Slackに送るPythonスクリプトを書いてください。
毎朝動かしたいです。

このスクリプトを実行すると、次のエラーで止まることがあります。

requests.exceptions.SSLError: HTTPSConnectionPool(host='example.com', port=443): Max retries exceeded ...
(Caused by SSLError(SSLCertVerificationError(1, '[SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed: unable to get local issuer certificate ...')))

このエラーを貼って「直して」とだけ頼むと、次のようなコードが返ってくることがよくあります。

import requests
import urllib3
from bs4 import BeautifulSoup

urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)

WEBHOOK = "https://example.com/hooks/xxxxxxxxxxxx"
URLS = ["https://example.com/item/1", "https://example.com/item/2"]

for url in URLS:
    try:
        r = requests.get(url, verify=False)
        soup = BeautifulSoup(r.text, "html.parser")
        price = soup.select_one(".price").text
        requests.post(WEBHOOK, json={"text": f"{url} {price}"}, verify=False)
    except Exception:
        pass

確かにエラーは出なくなります。ただしエラーが消えたのは、問題が解決したからではありません。「接続先が本物かどうか」を確かめる処理そのものを外したからです。

verify=False で失うもの

  • 取得したデータが本物か分からなくなる:証明書の検証は「接続先が本当に example.com か」を確かめる仕組みです。外すと、途中の通信経路で差し替えられたページでも受け取ってしまいます。価格監視なら、偽の価格を受け取っても気づけません。
  • 送信先の秘密も漏れうる:上のコードは Slack への post にまで verify=False が付いています。Webhook のURLは「知っていれば誰でも投稿できる鍵」です。検証しない通信で送れば、そのURLが盗まれても気づけません。
  • 証拠が残らない:disable_warnings() は「検証していない」という警告まで消します。ログを見ても、検証を外していることが分かりません。

AIが出したコードで、自分で確認すべき4箇所

コードを読み慣れていなくても、次の文字列を探すだけで危うい箇所は見つかります。エディタの検索機能か、grep -n "verify=False" *.py で確かめてください。

探す文字列

何が問題か

直させる方向

verify=False/disable_warnings/requests.packages.urllib3

証明書の検証を外し、警告を消している。requests.packages 経由の書き方は互換用に残っている古い経路で、古いコード例をなぞった兆候

検証は残したまま、証明書ストアの側を直す

timeout= が無い・sleep が無い

応答が無いと永久に待つ。連続アクセスで相手のサーバーに負荷をかける

timeout とURLごとの間隔を必須にする

except Exception: pass

失敗しても何も出さずに正常終了する。cronやsystemdから見ると毎日「成功」になる

ログに出し、失敗時は終了コード1で終わらせる

"https://..." や "sk-..." の直書き

Webhook・APIキーがファイルに残る。共有やバックアップで流出する

環境変数から読み、未設定なら起動時に止める

METR の研究では、熟練開発者でもAIの提案を採用したのは44%にとどまりました。また作業時間の約9%を、AIが生成したコードのレビューと修正に使っていました。書き慣れていない人ほど、この4箇所は機械的に確認する前提で使うのが安全です。

以前の書き方と今の書き方:certifi の更新と truststore.inject_into_ssl()

何が起きているのか(短く)

requests は標準では、OSの証明書ストア(OSが信頼する認証局の一覧)を使いません。代わりに certifi パッケージに同梱された一覧で接続先を確かめます。そのため、次のような場合に CERTIFICATE_VERIFY_FAILED になります。

  • OSやブラウザでは信頼されているのに、仮想環境(venv)の certifi が古いままになっている
  • 社内プロキシなど、OSにだけ登録された証明書を経由している

どちらの場合も、正しい直し方は「検証をやめる」ことではありません。検証に使う一覧のほうを正すことです。

以前(AIが出しがち)

今の書き方

SSLErrorへの対処

verify=False+urllib3.disable_warnings()

truststore.inject_into_ssl() でOSの証明書ストアを使う。または certifi を更新する

社内プロキシの証明書

検証を外す

OSに登録して truststore で読む。または REQUESTS_CA_BUNDLE に証明書ファイルを指定する

証明書の入れ替わりへの追随

そもそも検証していないので気づかない

OSの更新(ca-certificates)か pip install -U certifi で追随する

私は、自分で管理できるサーバー(VPSや自宅機)なら truststore を選びます。OSの更新で一覧も新しくなるので、venv の中身を気にせずに済むからです。共有レンタルサーバーのようにOSに手を出せない環境では、certifi を定期的に更新する方法が現実的です。

実行環境

  • Python:python3 -V で確認します。2026年9月30日に 3.14.8・3.13.16・3.12.15 が出ており、SSLContext の解放後メモリ利用の欠陥 CVE-2026-19445(CVSS v4.0 基本値 9.2・Critical)が修正されています。証明書まわりを扱うスクリプトなら、使っている系統の最新版に上げておくのが前提です。3.9 以前はサポートが終了しています。
  • パッケージ:pip install requests beautifulsoup4 truststore certifi
  • バージョン確認:pip show truststore certifi requests で確かめます。truststore が対応する Python の範囲は、公式ドキュメントで確認してください。

なお、2026年10月9日に Python 3.15.0 が出ました。PEP 602 により、新しいマイナー版は毎年10月に出ます。ただし定期実行しているスクリプトを出たその日に上げる必要はありません。急ぐべきなのは、上記のようなセキュリティ修正版のほうです。

直した後のコード

import logging
import os
import sys
import time

import truststore

truststore.inject_into_ssl()

import requests
from bs4 import BeautifulSoup

logging.basicConfig(
    level=logging.INFO,
    format="%(asctime)s %(levelname)s %(message)s",
)
log = logging.getLogger("price-watch")

WEBHOOK = os.environ.get("NOTIFY_WEBHOOK_URL")
if not WEBHOOK:
    sys.exit("NOTIFY_WEBHOOK_URL が設定されていません")

URLS = [
    "https://example.com/item/1",
    "https://example.com/item/2",
]
INTERVAL = 5
HEADERS = {"User-Agent": "price-watch/1.0 (contact: admin@example.com)"}


def fetch_price(session, url):
    r = session.get(url, headers=HEADERS, timeout=(5, 20))
    r.raise_for_status()
    soup = BeautifulSoup(r.text, "html.parser")
    node = soup.select_one(".price")
    if node is None:
        raise ValueError(f"価格の要素が見つかりません: {url}")
    return node.get_text(strip=True)


def main():
    failed = 0
    lines = []
    with requests.Session() as session:
        for i, url in enumerate(URLS):
            if i:
                time.sleep(INTERVAL)
            try:
                lines.append(f"{url} {fetch_price(session, url)}")
            except requests.exceptions.SSLError:
                log.exception("証明書の検証に失敗: %s", url)
                failed += 1
            except Exception:
                log.exception("取得に失敗: %s", url)
                failed += 1
        if lines:
            session.post(WEBHOOK, json={"text": "\n".join(lines)}, timeout=10).raise_for_status()
    log.info("完了: 成功 %d件 / 失敗 %d件", len(lines), failed)
    return 1 if failed else 0


if __name__ == "__main__":
    sys.exit(main())

ブロックごとに何をしているか

  1. truststore の呼び出し:truststore.inject_into_ssl() は、Python の ssl モジュールが検証に使う一覧をOSの証明書ストアに切り替えます。requests が通信を始める前に効かせたいので、ファイルの先頭で呼んでいます。検証そのものは有効のままで、verify=False はどこにもありません。
  2. ログの設定:時刻つきで出すので、cronやsystemdのログを見たときに「いつ・どのURLで」失敗したかが分かります。
  3. 環境変数の読み込み:Webhook のURLはコードに書かず、NOTIFY_WEBHOOK_URL から読みます。未設定なら、その場で理由を出して止めます。
  4. fetch_price():timeout=(5, 20) は「接続に5秒・応答に20秒」まで待つという意味です。raise_for_status() は、404や503を価格として読まずに例外にします。価格の要素が見つからないとき(サイト側のHTML構造が変わったとき)も、黙って空の値を送らずに例外にします。
  5. main():URLごとに INTERVAL 秒の間隔を空けます。SSLError だけは別のメッセージでログに残すので、証明書の問題かどうかがすぐ分かります。1件でも失敗すれば終了コード1を返すため、cronやsystemdの側で「失敗した」と記録されます。

AIに検証を外させずに直させる指示文(実物)

直させる指示

「直して」だけでは、AIは最短でエラーを消す方法(=検証を外す方法)を選びがちです。やってはいけないことを先に書き、方針を指定すると、上のコードに近いものが返ってきます。

次のPythonスクリプトを直してください。条件は以下です。

1. verify=False と urllib3.disable_warnings() は使わないでください。
   証明書の検証は必ず有効のままにします。
2. SSLError への対処は「OSの証明書ストアを使う」方針にしてください。
   truststore パッケージの truststore.inject_into_ssl() を、
   requests を使う前にスクリプトの先頭で呼んでください。
3. requests.get には timeout を付け、URLごとに5秒以上の間隔を空けてください。
4. except: pass で例外を握りつぶさないでください。
   ログに出したうえで、1件でも失敗したら終了コード1で終わるようにしてください。
5. 通知先のURLはコードに書かず、環境変数 NOTIFY_WEBHOOK_URL から読んでください。
   未設定なら起動時に止めてください。
6. 変更した箇所ごとに、なぜ変えたかを1行で説明してください。

(ここにスクリプトを貼る)

6番を入れておくと、AIが黙って別の変更を混ぜたときに気づけます。調査では「自分で説明できないコードはコミットしない」という原則が挙げられていました。その説明をAI自身に書かせるわけです。

AIがまた検証を外してきたときの返し方

指示しても、「念のため」として verify=False を残したり、「うまくいかない場合は」と注記付きで戻してきたりすることがあります。その場合は、理由を添えて差し戻します。

verify=False が残っています。これは削除してください。
証明書の検証を外すと、接続先が本物か確かめられず、
通知先のWebhook URLも検証なしの通信で送ることになります。

truststore を使っても SSLError が出る場合は、検証を外す代わりに、
原因を調べるためのコマンドを提示してください。
(例:openssl で接続先の証明書チェーンを表示する)

社内プロキシ経由などで、どうしても特定の証明書を追加したい場合もあります。そのときは「REQUESTS_CA_BUNDLE にその証明書ファイルのパスを指定する方法で」と指定させます。これなら検証は残したまま、信頼する相手だけを増やせます。

直った後に自分で確かめること

  • grep -n "verify=False\|disable_warnings" *.py の結果が空であること
  • openssl s_client -connect example.com:443 -servername example.com </dev/null の出力で、Verify return code: 0 (ok) になっていること(OS側では検証が通っていることの確認)
  • NOTIFY_WEBHOOK_URL を設定せずに実行し、起動時に止まること
  • URLを1つ存在しないパスに変えて実行し、echo $? が 1 になること(失敗が終了コードに出ることの確認)

取得先サイトへの配慮:robots.txt・利用規約・アクセス間隔

価格監視は毎日同じページにアクセスするので、相手のサーバーから見ると「毎日来る自動アクセス」です。次の点は、AIに書かせる前に自分で確かめてください。

  • robots.txt:https://example.com/robots.txt を開き、取得したいパスが Disallow になっていないかを見ます。
  • 利用規約:自動取得を禁じているサイトもあります。公式APIや商品データの提供があるなら、そちらを使います。
  • アクセス間隔と頻度:上のコードではURLごとに5秒空けています。価格の確認なら1日1〜2回で足ります。「変化をすぐ知りたいから毎分」のような頻度にはしません。
  • User-Agent に連絡先:何のアクセスかを相手が判断できるようにします。
  • 制限は回避しない:アクセスを拒否されたら、そこで止めます。IPやUser-Agentを変えて続けるような書き方は、AIが提案してきても採用しません。

cron・systemd timer・VPS:証明書の更新で止まらずに毎日動かす条件

手元で動いたら、次は「どこで毎日動かすか」です。証明書まわりで止まる原因は、どの環境でも「検証に使う一覧が古くなること」と「失敗に気づかないこと」の2つです。

3つの置き場所と、止まる条件

置き場所

証明書の一覧の更新

止まる条件

手元のPC

OSの更新で追随(truststore)

スリープ・電源オフ・持ち出しで、その時刻に動かない

共有レンタルサーバーのcron

OSに手を出せないので、pip install -U certifi を定期的に実行する

certifi が古いまま放置されると、ある日から SSLError になる。cronは環境変数を読み込まないので、Webhook のURLが未設定になりやすい

VPS・自宅サーバー(systemd timer)

OSの ca-certificates の更新で追随(truststore)

OSの更新を止めていると一覧が古くなる。停止中に逃した回は、設定しないと実行されない

常時動いていて、OSの更新を自分で管理できるVPSか自宅機が一番止まりにくい選択肢です。共有サーバーでcronが使えるなら、certifi の更新を月1回のcronに入れておけば十分運用できます。

共有サーバーのcronで動かす場合

0 9 * * * . /home/username/price-watch/.env; /home/username/price-watch/.venv/bin/python /home/username/price-watch/watch.py >> /home/username/price-watch/watch.log 2>&1
0 4 1 * * /home/username/price-watch/.venv/bin/pip install -U certifi >> /home/username/price-watch/watch.log 2>&1

1行目は毎朝9時の実行です。.env には export NOTIFY_WEBHOOK_URL=... と書き、chmod 600 で自分だけが読めるようにします。2行目は毎月1日の certifi の更新です。この環境ではOSの一覧が使えないため、truststore ではなく certifi に頼ることになります。

VPS・自宅サーバーで systemd timer を使う場合

# /etc/systemd/system/price-watch.service
[Unit]
Description=price watch

[Service]
Type=oneshot
User=username
WorkingDirectory=/home/username/price-watch
EnvironmentFile=/home/username/price-watch/.env
ExecStart=/home/username/price-watch/.venv/bin/python watch.py

# /etc/systemd/system/price-watch.timer
[Timer]
OnCalendar=*-*-* 09:00
Persistent=true
RandomizedDelaySec=10min

[Install]
WantedBy=timers.target

systemd の EnvironmentFile は export を付けず、NOTIFY_WEBHOOK_URL=... の形で書きます。Persistent=true を付けると、停止中に逃した回が起動時に実行されます。RandomizedDelaySec は、実行時刻を毎回少しずらして、毎日ぴったり同じ時刻にアクセスしないようにする設定です。cron との違いや取りこぼし対策は systemd timerでcron取りこぼし対策|26年10月 で詳しく書いています。

truststore を使う場合、証明書の一覧はOSの ca-certificates パッケージで更新されます。Ubuntu・Debian なら dpkg -l ca-certificates でバージョンを確認し、OSのセキュリティ更新を止めないでおくことが条件です。Python 本体も、9月30日の CVE-2026-19445 の修正のような更新は早めに当ててください。

失敗に気づける形にしておく

  • スクリプトは失敗時に終了コード1を返すので、systemctl list-units --failed や journalctl -u price-watch.service で失敗が見えます。
  • ログに「証明書の検証に失敗」が出たら、それは検証が働いた証拠です。verify=False を足すのではなく、OSの更新か certifi の更新を先に試します。
  • 通知が毎朝届くこと自体を、動いている確認に使えます。届かない日があれば、何かが止まっています。

関連する選択肢

毎日決まった時刻にスクリプトを動かすだけなら、共有のレンタルサーバーでも足ります。cronが使えるプランを選べば、自分でOSを管理する必要はありません。

レンタルサーバー エックスサーバー

※ 広告を含みます(A8.net)。リンク経由でお申し込みがあった場合、手数料を受け取ることがあります。

まとめ:エラーを消すのではなく、検証を残して直させる

ChatGPTやClaude Codeは、SSLError に対して verify=False と urllib3.disable_warnings() で「動いた」ことにしがちです。しかしそれは、接続先の確認と警告を消しただけです。直すときは、AIへの指示で検証を外すことを先に禁じます。そのうえで、truststore.inject_into_ssl() でOSの証明書ストアを使うか、certifi を更新させます。同じ指示の中で、timeout・アクセス間隔・失敗時の終了コード1・環境変数からのURL読み込みも求めておくと、確認すべき4箇所がまとめて片づきます。毎日動かすなら、OSを自分で更新できるVPSや自宅機の systemd timer に置き、truststore でOSの更新に追随させるのが最も止まりにくい形です。共有サーバーのcronなら、certifi の月次更新を一緒に登録しておきます。