0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

PowerShell 5.1の「-Encoding UTF8」はBOM付き:実際にハマる3例と回避策

0
Posted at

はじめに

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進ダンプが一番確実。

ハマった点

  1. コンソールでは「同じ文字列」に見える。文字列比較やログでは気づけず、repr() やバイトダンプで初めて見える。
  2. strip() では消えない。BOMは空白文字ではないので、strip("\ufeff") か utf-8-sig を使う。
  3. 「書く側」と「読む側」の非対称が罠。PowerShellで書いたファイルをPythonやHTTPで読む構成では、読む側がBOMを想定していないと壊れる。チーム内でファイルを共有するときは書き込み方法を統一する。
  4. 補足: 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章は無料で読めます。

Windowsで動かすローカルLLM自動化:Ollama実測ガイド

0
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?