はじめに
Windows PowerShell 5.1 の Set-Content -Encoding UTF8 は、BOM(EF BB BF)付きのUTF-8でファイルを書き出す。BOMは画面に表示されないため、「同じ文字列を書いたはず」なのに読み込む側で認証失敗やパースエラーの原因になる。実際に遭遇した3例と、バイト単位の確認方法・回避策をまとめる。
環境
- Windows 11(ビルド 10.0.26200)
- Windows PowerShell 5.1
- OpenSSH(
ssh-keygen) - Python 3.11
やったこと
1. 「BOMが付く」ことをバイトで確認する
Set-Content -Path .\sc_utf8.txt -Value "token1234" -Encoding UTF8
[System.IO.File]::WriteAllText("$PWD\wal.txt", "token1234")
# 先頭5バイトを16進で表示
([System.IO.File]::ReadAllBytes("$PWD\sc_utf8.txt")[0..4] | ForEach-Object { $_.ToString("X2") }) -join " "
([System.IO.File]::ReadAllBytes("$PWD\wal.txt")[0..4] | ForEach-Object { $_.ToString("X2") }) -join " "
実行結果:
EF BB BF 74 6F <- Set-Content -Encoding UTF8(BOM付き)
74 6F 6B 65 6E <- [IO.File]::WriteAllText(BOMなし)
Get-Content で中身を表示しても「同じ文字列」に見えるので、バイトで見るのが確実だ。
2. 事故例1: APIトークン — HTTPヘッダへのBOM混入
Set-Content -Encoding UTF8 で保存したトークンファイルをPythonで読む:
token = open("sc_utf8.txt", encoding="utf-8").read()
print(repr("Bot " + token))
# 'Bot \ufefftoken1234\n'
print(("Bot " + token).encode("utf-8")[:10].hex(" "))
# 42 6f 74 20 ef bb bf 74 6f 6b
Authorization ヘッダの値に EF BB BF がそのまま混ざり、サーバー側から見ると別のトークンになる(認証失敗の原因)。
厄介なのは、strip() では消えないこと。
print(repr("\ufefftoken1234\n".strip()))
# '\ufefftoken1234' <- 改行は消えるがBOMは残る
BOM(U+FEFF)はPythonの空白文字扱いではないため、strip() の対象外になっている。
3. 事故例2: JSON設定ファイル — パースエラー
Set-Content -Path .\cfg.json -Value '{"name":"app","version":1}' -Encoding UTF8
これをPythonで読む:
utf-8 : ERROR: JSONDecodeError Unexpected UTF-8 BOM (decode using utf-8-sig): line 1 column 1 (char 0)
utf-8-sig: {'name': 'app', 'version': 1}
エラーメッセージ自体が utf-8-sig の使用を案内してくる。
4. 事故例3: SSH公開鍵 — 手元では「読めてしまった」
公開鍵をBOM付き・BOMなしで保存して比較する:
Set-Content -Path .\bad_auth -Value (Get-Content .\key.pub) -Encoding UTF8
[System.IO.File]::WriteAllText("$PWD\good_auth", (Get-Content .\key.pub -Raw), [System.Text.UTF8Encoding]::new($false))
bad head : EF BB BF 73 73 <- 先頭にBOM("ss"の前に3バイト)
good head: 73 73 68 2D 65 <- "ssh-e"
ssh-keygen -l -f で指紋を表示してみると、BOM付きでも正常に読み取れてしまった。WindowsのOpenSSH実装は先頭BOMを許容するようだ。
ただし、これは「この組み合わせのバージョンでは許容された」だけ。鍵ファイルは配布・転送してサーバー側で使われるもので、実際に筆者は、文字列パイプ経由でSSH転送した公開鍵が受信側(Linux)の authorized_keys にBOM混入し、鍵認証が通らなくなった事例を経験している。手元の ssh-keygen が読めたことは「安全」を意味しない。鍵のような機械可読ファイルは、読み書きともBOMなしで統一しておくのが安全だ。
回避策
書くとき(BOMなしUTF-8)
# 明示的にBOMなしUTF-8で書き込む
[System.IO.File]::WriteAllText($path, $text, [System.Text.UTF8Encoding]::new($false))
# ASCIIの内容(トークン・鍵・設定値など)ならこれでもよい
Set-Content -Path $path -Value $text -Encoding Ascii
読むとき
# Python側: BOMがあっても自動で読み飛ばす
open(path, encoding="utf-8-sig").read()
# PowerShell側: 読んでから先頭のBOM文字を除去
(Get-Content -Raw $path).TrimStart([char]0xFEFF)
確認するとき
先頭3バイトが EF BB BF ならBOM付き。先頭3バイトの16進ダンプが一番確実。
ハマった点
-
コンソールでは「同じ文字列」に見える。文字列比較やログでは気づけず、
repr()やバイトダンプで初めて見える。 -
strip()では消えない。BOMは空白文字ではないので、strip("\ufeff")かutf-8-sigを使う。 - 「書く側」と「読む側」の非対称が罠。PowerShellで書いたファイルをPythonやHTTPで読む構成では、読む側がBOMを想定していないと壊れる。チーム内でファイルを共有するときは書き込み方法を統一する。
- 補足: PowerShell 7 系では
-Encoding utf8の既定がBOMなしに変わっており、同じスクリプトでも 5.1 と 7 で挙動が異なる点にも注意。
まとめ
- Windows PowerShell 5.1 の
Set-Content -Encoding UTF8はBOM付き。まずバイトで確認する癖をつける。 - 実害は「APIヘッダへの混入(認証失敗)」「JSONパースエラー」「鍵ファイルへの混入」。
- 回避は、書き込みを
[IO.File]::WriteAllText(..., UTF8Encoding($false))、読み込みをutf-8-sig/TrimStart([char]0xFEFF)に統一すること。
関連する本
文字コードのつまずきを含む運用設計を一冊にまとめました。第1章・第2章は無料で読めます。