2026年10月に公表された漏えいの第一報では、範囲の示し方が「最大〇件」「現時点で確認できている期間」「〇日時点で約〇件」になっていました。実際に誰が、いつ、何件を持ち出したかは、それを記録していた分しか答えられません。

連載「事故から逆算するWebセキュリティ」の今回は、事故のあとの調査に使える監査ログの設計を扱います。記録するイベント、1行に入れる項目、残してはいけない値、改ざんされない置き場所の順に整理し、ハッシュチェーンで改ざんを検知するロガーを実際に動かします。

WHO, WHEN, WHAT事故のあとの問い

アクセスログでは答えられない問いに、監査ログで答える

アクセスログだけ

Webサーバーの既定の記録

  • 誰が:IPアドレスまで
  • 何を:URLとステータスだけ
  • 何件読んだか:分からない
  • 既定の保持日数で消える

監査ログを設計する

アプリが業務の操作ごとに書く

  • 誰が:利用者IDとセッション
  • 何を:操作名と対象のID
  • 何件読んだか:件数を記録
  • 別の置き場所に改ざんできない形で
項目の考え方: OWASP Logging Cheat Sheet(when・where・who・what)

30秒でわかる(2026年10月17日時点)

  • ・記録するもの:認証の成功と失敗、認可の失敗、権限の変更、APIキーの発行、設定変更、データの書き出しと大量の読み出し
  • ・1行の項目:UTCの時刻、利用者ID、ハッシュ化したセッションID、送信元IP、操作、対象のID、結果、リクエストID、件数
  • ・残さない値:パスワード、トークン、APIキー、カード番号。ロガーの入口で伏せる
  • ・置き場所:アプリと別のアカウントに追記だけで送り、WORMで保管。ハッシュチェーンは末尾の控えと組み合わせる

セキュリティ全体の地図は サプライチェーン攻撃とは|手口・防御ツール比較・npm 12の新機構まで実践解説 にまとめています。今あるログのどこを見るかと、主なクラウドの既定の保持期間は IPAの不正アクセス対策を実践する手順 で扱いました。本記事はその前の段階、つまり調査ができる記録をアプリにどう残させるかの話です。

事故に気づくきっかけもログです。デジタル庁は6月25日、保守運用担当者のアカウントでサーバー上の大量のファイルにアクセスされたことを検知して調査を始め、7月9日に VPN 機器の脆弱性を悪用した侵入だと突き止めました。アフラックは7月31日の調査結果で、短時間に大量のデータ照会があったときの監視・制御の機能が足りなかったと説明しています。どちらも「誰が、どれだけ読んだか」を記録し、普段と比べられることが前提になります。

監査ログが無いと、事故の範囲を「最大」でしか言えない

10月の事案の第一報と、IPAの点検の求めを時系列で並べます。

日付 出来事 範囲の示し方
10-09 IPAが不正アクセス対策の注意喚起 ログは直近1か月、その後3か月へ広げて確認するよう求める
10-15 ソースネクストが漏えいを公表 10月13日13時36分までの会員登録者、最大22,519,960件
10-16 Studioが第一報を公表 期間は「現時点で確認できている」9月30日〜10月1日、件数は精査中
10-16 日本経済新聞社がシステム開発に利用するクラウドサービスの件を公表 10月16日時点で約183万件

ソースネクストの件は、ソースネクスト情報漏洩|最大2,252万件・パスワードも対象 で読んだとおり、対象が「登録していた全員」で示され、実際に取得された件数は発表の範囲では分かっていません。Studioの件も、Studio不正アクセスで利用者がやること のとおり期間に「現時点で」が付いています。調査の途中で数字が動くのは当然です。ここで問いたいのは自社の側で、同じことが起きたら次の問いに何日で答えられるかです。

事故のあとに聞かれる問い 答えるのに要る記録
誰のアカウントが使われたか 認証の成功と失敗、送信元IP、セッション
いつからいつまでか すべての行のUTCの時刻と、その期間のログが残っていること
何を、何件持ち出したか 書き出しと読み出しの操作名、対象のID、件数
入口は何か 権限の変更、APIキーの発行、設定変更
ログ自体は消されていないか 改ざんの検知と、別の置き場所の控え

クラウドの監査ログがあれば足りる、とは言えません。AWS CloudTrail の証跡は既定で管理イベントだけを記録し、S3のオブジェクトの読み出しなどのデータイベントは記録しません(追加料金で有効にできます)。Google Cloud のデータアクセス監査ログも、BigQueryの一部を除いて既定で無効です。アプリの画面やAPIから顧客データを読み出した操作は、アプリが書かない限りどこにも残りません。

監査ログに記録するイベント

OWASP Logging Cheat Sheet は「必ず記録するもの」として、認証の成功と失敗、認可の失敗、セッション管理の失敗、利用者管理と権限の変更、管理者の操作、データの取り込みと書き出し(画面上の帳票を含む)、暗号鍵の使用とローテーションを挙げています。OWASP Logging Vocabulary Cheat Sheet は、それぞれに authn_login_fail のようなイベント名を付けています。

イベント OWASP の語彙の例 事故のあとで答える問い
ログインの成功と失敗 authn_login_success / authn_login_fail 総当たりの後に成功した ID はどれか
認可の失敗 authz_fail 他人のデータを ID を変えて取りに来たか
権限の変更・管理者の操作 authz_change / authz_admin / privilege_permissions_changed 攻撃者が自分を管理者にしたか
APIキー・トークンの発行と失効 authn_token_created / authn_token_revoked 居座るための鍵を作られたか
機微なデータの読み出しと書き出し sensitive_read ほか 誰が何件を持ち出したか
設定変更・監視の停止 sys_monitor_disabled 記録を止めてから動いたか

失敗だけでなく成功も残します。OWASP Top 10:2025 の A09(Security Logging & Alerting Failures)は、ログインの成功だけを記録して失敗を残さない、のような一貫しない記録を典型的な欠陥に挙げています。逆向きの欠け、つまり失敗だけを残して成功を残さない設計も危険です。総当たりの末に1回通ったログインを後から特定できません。

書き出しの記録には件数を入れます。CSV出力や一括取得のAPIで「誰が」だけ残っても、漏えいの規模は答えられません。

監査ログの項目設計:1行に入れるフィールド

OWASP は、各行に「いつ・どこで・誰が・何を」を入れるよう求めています。アプリの監査ログに落とすと次のとおりです。

項目 例 入れる理由
ts 2026-10-12T07:54:37.200+00:00 ISO 8601、UTCで統一する。全サーバーの時刻をNTPでそろえる
actor u-2001 利用者のID。表示名やメールアドレスでなく、変わらない主キー
session_id sha256:76cb4332… 同じセッションの操作を束ねる。値そのものは残さずハッシュにする
source_ip / user_agent 203.0.113.19 見慣れない場所や道具からの操作を拾う
action data.export 操作名。語彙を決めて、表記をぶらさない
target customers/5 操作の対象のID。範囲の特定はここから
result success / fail 失敗の理由も別の項目に残す
request_id req-00123 Webサーバー・アプリ・DBのログを1つの操作で結ぶ
rows 13520 読み出し・書き出しの件数

リクエストIDは、入口で1つ振って下流のサービスへヘッダーで渡します。OWASP が「相互作用の識別子」と呼ぶもので、後から時刻とIPで突き合わせる手間を省けます。

形式は1行1件のJSONにします。ログ基盤がそのまま読めるうえ、改行や区切り文字を値に混ぜてログを偽装するログインジェクション(CWE-117)も、JSONとして符号化すれば成立しません。

残さない値とマスキング

OWASP は、ログに直接記録しないものとして、パスワード、セッションID(必要ならハッシュに置き換える)、アクセストークン、DBの接続文字列、暗号鍵、カード会員データ、機微な個人情報を挙げています。A09 には CWE-532(ログへの機微な情報の書き込み)も入っています。

伏せる処理は、呼び出し側の気配りに任せずロガーの入口に置きます。キー名で一律に伏せ、自由記述の欄はカード番号の形だけを末尾4桁に縮めます。IPアドレスも組み合わせると個人を特定できる情報になりうる、と OWASP の語彙のシートは注意しています。残すかは調査で使う重みと比べて決めます。

改ざんされない置き場所と、保持期間の決め方

侵入者が最初に消したがるのはログです。OWASP は保管時の対策として、改ざんの検知を組み込むこと、できるだけ早く読み取り専用の媒体に写すこと、ログへのアクセス自体を記録することを挙げています。A09 も、追記だけのテーブルなどの改ざん対策を持った監査証跡を求めています。

HASH CHAIN改ざん検知の仕組み

1件ごとに前の行のハッシュを入れて鎖にする

  1. 01

    1件目は prev に 0 を64個

    記録全体の SHA-256 を hash に入れる

  2. 02

    2件目の prev に1件目の hash

    以降も前の行の hash を引き継ぐ

  3. 03

    1行でも書き換えると鎖が切れる

    検証はその行の番号を返す

  4. 04

    末尾の hash を別の場所に控える

    末尾の削除と、鎖ごとの作り直しに備える

CloudTrail のダイジェストファイルも、前のダイジェストの署名を含めて鎖にしている(AWS CloudTrail ユーザーガイド)

・置き場所を分ける:アプリと同じサーバー・同じアカウントに置くと、アプリを乗っ取った攻撃者がログも消せます。ログ専用のアカウントやプロジェクトに送り、アプリ側の権限は書き込みだけにします
・WORMで保管する:S3 Object Lock は、保持期間の間オブジェクトの上書きと削除を止めます。バージョニングを有効にしたバケットでだけ使えます
・鎖と署名で検知する:CloudTrail のログファイル整合性の検証は、1時間ごとのダイジェストファイルに各ログのSHA-256を入れて署名し、前のダイジェストの署名も含めて鎖にしています。アプリのログにも同じ考え方を小さく持ち込めます
・止まったことに気づく:OWASP は、ログが止まったことを検知する仕組みを求めています。記録の件数が平時より急に落ちたら警告します

保持期間は、長いほど調査に有利で、長いほど個人情報を抱えるリスクが増えます。OWASP は、決めた保持期間より前に消さないことと、それより長く持たないことの両方を求めています。IPAが直近1か月、その後3か月と段階的に広げて見るよう求めた以上、少なくとも3か月分はすぐ検索できる場所に置き、それより古い分は安い保管先に移す、というのが当サイトの判断です。法令や契約の期間があればそちらに合わせます。

個人情報保護委員会は2026年10月7日に大規模な漏えい事案を踏まえた注意喚起を出し、ガイドライン(通則編)の安全管理措置の「手法の例示」を見直す予定を示しています。確定は2027年4月の予定とされています。ログに関する記述の中身は未確認です。

ログ管理の総論である NIST SP 800-92 は2006年の版が最終版で、改訂版の SP 800-92 Rev. 1(Cybersecurity Log Management Planning Guide)は2023年10月11日に初期公開草案が出たまま、2026年10月17日時点でも草案です。

ハッシュチェーンの監査ログをPythonで実装して改ざん検知を確かめる

ここまでの設計を、標準ライブラリだけのロガーにしました。伏せ字の処理、UTCの時刻、前の行のハッシュを1行ずつ入れる鎖、検証関数の4つです。

import datetime, hashlib, json, re

SECRET = re.compile(r"pass(word)?|token|secret|authorization|cookie|api_?key", re.I)
CARD = re.compile(r"\b(?:\d[ -]?){12,15}(\d{4})\b")

def mask(v, k=""):
    if isinstance(v, dict):
        return {kk: mask(vv, kk) for kk, vv in v.items()}
    if SECRET.search(k):
        return "[REDACTED]"
    if k == "session_id":  # 追跡に使えるようハッシュに置き換える
        return "sha256:" + hashlib.sha256(v.encode()).hexdigest()[:16]
    return CARD.sub(r"****\1", v) if isinstance(v, str) else v

def canon(rec):
    return json.dumps(rec, sort_keys=True, ensure_ascii=False, separators=(",", ":")).encode()

class AuditLog:
    def __init__(self, path):
        self.path, self.prev = path, "0" * 64
        try:
            with open(path, encoding="utf-8") as f:
                for line in f:
                    self.prev = json.loads(line)["hash"]
        except FileNotFoundError:
            pass

    def write(self, actor, action, target, result, at=None, **ctx):
        at = at or datetime.datetime.now(datetime.timezone.utc)
        rec = {"ts": at.isoformat(timespec="milliseconds"), "actor": actor,
               "action": action, "target": target, "result": result,
               **mask(ctx), "prev": self.prev}
        rec["hash"] = hashlib.sha256(canon(rec)).hexdigest()
        with open(self.path, "a", encoding="utf-8") as f:  # 追記だけ
            f.write(json.dumps(rec, ensure_ascii=False) + "\n")
        self.prev = rec["hash"]

def verify(path, anchor=None):
    prev = "0" * 64
    with open(path, encoding="utf-8") as f:
        for n, line in enumerate(f, 1):
            rec = json.loads(line)
            h = rec.pop("hash")
            if rec["prev"] != prev or hashlib.sha256(canon(rec)).hexdigest() != h:
                return f"NG: {n}行目で鎖が切れています"
            prev = h
    if anchor and anchor != prev:
        return "NG: 末尾が別の場所に控えたハッシュと合いません"
    return f"OK: {n}件"

これを audit_log.py として保存し、45日分・240件のサンプル(ログイン、認可の失敗、書き出し、権限の変更などを固定の乱数で混ぜたもの)を書かせました。最後の行のハッシュは anchor.txt に控えます。本番ならこの控えを、ログとは別のアカウントやWORMの保管先に定期的に送ります。

手を加えないログ(陰性対照)と、120行目の actor だけを書き換えた写し(陽性対照)を検証した結果です。

python3 -c 'import audit_log as a; print(a.verify("audit.jsonl", open("anchor.txt").read().strip()))'
sed '120s/"actor": "[^"]*"/"actor": "u-1001"/' audit.jsonl > tampered.jsonl
python3 -c 'import audit_log as a; print(a.verify("tampered.jsonl", open("anchor.txt").read().strip()))'
# OK: 240件
# NG: 120行目で鎖が切れています

同じ手順で、行の削除、末尾の削除、書き換えたうえで鎖ごと計算し直す改ざんも試しました。OK と出ても、記録されなかった操作や、ログに書く前に消された操作が無かったとは限らない点に注意してください。この検証で分かるのは、書かれた行が後から変えられていないかだけです。

MEASURED実測

鎖だけでは末尾の削除と作り直しを見逃す

240件のログにしたこと

検証の結果

240件のログにしたこと: 何もしない(陰性対照)

検証の結果: OK: 240件

240件のログにしたこと: 120行目の actor を書き換え

検証の結果: NG: 120行目で検出

240件のログにしたこと: 150行目を削除

検証の結果: NG: 150行目で検出

240件のログにしたこと: 末尾1行を削除

検証の結果: 控えなし OK(見逃す)/控えあり NG

240件のログにしたこと: 書き換えて鎖ごと計算し直す

検証の結果: 控えなし OK(見逃す)/控えあり NG

実測: Python 3.13.16、当サイトの検証用ロガー(2026-10-17)。控え=末尾の hash を別の場所に保存したもの

鎖だけでは、末尾の削除と鎖ごとの作り直しを見逃しました。ファイル全体を書き換えられる攻撃者は、ハッシュも全部計算し直せるからです。末尾のハッシュを攻撃者の手の届かない場所に控えたときだけ、両方を検知できました。CloudTrail がダイジェストに署名し、S3 Object Lock が上書きを止めるのも、同じ穴を塞ぐためです。

伏せ字の処理は、password・access_token・api_key、入れ子の headers の Authorization と Cookie、session_id、自由記述に混ぜたカード番号 4111 1111 1111 1111 を渡して確かめました。書かれた行に元の値は1つも残らず、request_id はそのまま残り、カード番号は ****1111 になりました。

最後に、事故のあとに最初に聞かれる「直近30日に誰がデータを書き出したか」を、同じサンプルに聞きます。

jq -sc --arg since "$(date -u -d '30 days ago' +%FT%T)" '[.[] | select(.action == "data.export" and .ts >= $since)] | group_by(.actor)[] | {actor: .[0].actor, times: length, rows: (map(.rows) | add), last: (map(.ts) | max)}' audit.jsonl
# {"actor":"admin-01","times":3,"rows":66295,"last":"2026-10-12T07:54:37.200+00:00"}
# {"actor":"u-1001","times":5,"rows":60320,"last":"2026-10-12T12:24:37.200+00:00"}
# {"actor":"u-1002","times":3,"rows":61345,"last":"2026-10-12T21:24:37.200+00:00"}
# {"actor":"u-2001","times":6,"rows":139648,"last":"2026-10-13T15:24:37.200+00:00"}

全期間の書き出し25件のうち直近30日は17件で、Pythonで日時として比べた結果と一致しました。この比較は時刻の文字列を並べ替えているだけなので、すべての行がUTCで書かれていることが前提です。試しに +09:00 の行を1件混ぜると、締め日より前の書き出しが範囲に入りました。時刻をUTCで統一する理由は、この1行の集計が正しく動くことにあります。date -d はGNUの書き方で、macOSでは date -u -v-30d +%FT%T に置き換えます。

対策:監査ログを設計するときのチェックリスト

・イベントの一覧を先に決める:上の表を元に、自社の操作をイベント名に割り当てます。書き出しと一括取得のAPIには件数を入れます
・項目と形式をそろえる:UTCの時刻、変わらない利用者ID、リクエストID。1行1件のJSONで出し、サービスごとに表記をぶらさない
・伏せる処理をロガーに置く:キー名で伏せ、セッションIDはハッシュにします。新しい項目を足すたびに、伏せ字のテストも1件足します
・別の場所に追記だけで送る:ログ専用のアカウントへ送り、WORMで保管し、末尾のハッシュかダイジェストを控えます
・クラウド側の記録も足す:CloudTrail のデータイベントや Google Cloud のデータアクセス監査ログを、顧客データの置き場所に限って有効にします
・警告を最小限で始める:ログイン失敗の急増、管理者権限の付与、APIキーの発行、平時を大きく超える件数の書き出し、ログの件数の急減の5つから。A09 は、本番では使わないハニートークン(おとりのアカウントやデータ)を置き、触れられたら警告する方法も挙げています

自システム確認コマンド

自社の監査ログに対して、上の jq を action の名前だけ置き換えて流してください。数分で「直近30日に誰が何件書き出したか」が出ないなら、記録が足りていません。出なかった理由が、書き出しを記録していない、件数が無い、30日分が残っていない、のどれかを確かめるところから始めます。

影響範囲

対象は、ログインのあるWebアプリやAPIを運用し、顧客の個人情報を画面・CSV・APIで読み出せるようにしているすべての組織です。今回動かしたロガーは検証用の小さなもので、複数のプロセスからの同時書き込み、ログのローテーションをまたぐ鎖、ログ基盤への転送、控えの自動送信は確かめていません(未検証)。本番では、利用しているログ基盤やクラウドの改ざん対策(CloudTrail の整合性の検証、S3 Object Lock など)と組み合わせてください。

本記事の実測環境:Linux x86_64、Python 3.13.16(標準ライブラリのみ)、jq 1.7、GNU coreutils の date。2026年10月17日に当サイトで実行しました。

参照ソース