UrsaKit 封面圖:IPv4 位址的 32 個位元,以及 127.0.0.1 的四種寫法

IPv4 位址是一個 32 位元數字。大家熟悉的 192.168.1.1 只是把這個數字寫下來的一種方式:四個十進位 octet,用點分隔。至少還有五種寫法可以指向同一組 32 位元,而且瀏覽器、URL 函數庫與命令列工具大多仍然接受。

這不只是冷知識。如果你的應用程式是用字串比對來封鎖位址,攻擊者就能自己選擇要送哪一種等價寫法。本文說明這些表示法的意義、用真實解析器驗證行為,並示範能補上這個缺口的正規化步驟。

01IPv4 位址就是一個 32 位元數字

點分十進位其實是 256 進位的數字表示法。四個 octet 各帶 8 位元,第一個 octet 是最高位。把整串當成單一整數來讀,就會得到 0 到 4294967295 之間的值。

點分十進位整數十六進位
0.0.0.000x00000000
10.0.0.11677721610x0a000001
127.0.0.121307064330x7f000001
169.254.169.25428520391660xa9fea9fe
192.168.1.132322357770xc0a80101
255.255.255.25542949672950xffffffff

本文其他所有表示法,都只是這些整數的另一種編碼。抓住這一點,整個主題就變得可預測。

02手算:點分十進位轉整數

這個轉換是位置記數法,跟十進位完全一樣。把每個 octet 乘上 256 的次方(從右邊算起),再加總:

192.168.1.1
192 x 256^3 = 192 x 16777216 = 3221225472
168 x 256^2 = 168 x    65536 =   11010048
  1 x 256^1 =   1 x      256 =        256
  1 x 256^0 =   1 x        1 =          1
                              ----------
                              3232235777

127.0.0.1 用同樣的算法會得到 2130706433,也就是標題裡那個數字。兩者都在 2026-10-04 以 Python 3 實測:

python3 -c "import ipaddress; print(int(ipaddress.IPv4Address('192.168.1.1')),
  int(ipaddress.IPv4Address('127.0.0.1')))"

反方向用十六進位讀更快。把 32 位元值切成四個位元組,再把每個位元組轉成十進位:0xc0a80101 是 c0、a8、01、01,也就是 192、168、1、1。

03URL 解析器仍接受的舊式寫法

RFC 3986 很嚴格。它的文法只允許四個十進位 octet、而且開頭不能有 0;但作者也知道實作沒那麼嚴格:規範裡有一整節專門談罕見的 IP 位址格式,警告 gethostbyname、inet_aton 這類平台函數接受的形狀遠多於文法描述。

瀏覽器實作的 WHATWG URL 標準,把這種寬容寫成了算法。host 解析器會用點切開輸入,並接受一到四段:

  • 四段:每一段是一個 octet,由左到右排列。
  • 三段:前兩段是 octet,最後一段變成 16 位元值,放在最後兩個位元組。
  • 兩段:第一段是 octet,最後一段變成 24 位元值,放在最後三個位元組。
  • 一段:這個值就是整個 32 位元位址。

每一段接著以 C 風格前綴解析:開頭 0x 代表十六進位,開頭 0 代表八進位,其餘是十進位。大於 255 的段落只能出現在最後一段,而且最後一段必須塞得進剩下的位元組。

這些寫法就是這樣來的。單一數字 2130706433 就是整個位址。0x7f.1 是十六進位的 0x7f 加上 1,補滿剩下的三個位元組。017700000001 是同一個值的八進位。0300.0250.0.1 會變成 192.168.0.1,因為 0300 與 0250 是八進位。它們都不是不同位址。

開頭為 0 的規則有一個讓人意外的後果:在 URL 解析器裡,010.1.1.1 不是 10.1.1.1,而是 8.1.1.1。這個格式在網絡上存活得夠久,改掉會弄壞真實網站,所以相容算法才會存在。

04實測:同一個位址的五種寫法

以下輸出來自三個不同解析器,全部在 2026-10-04 執行。Node.js 24.14.1 實作 WHATWG URL 標準:

node -e "for (const s of ['http://2130706433/','http://0x7f.1/',
  'http://017700000001/','http://127.1/']) console.log(s, new URL(s).hostname)"
http://2130706433/             -> 127.0.0.1
http://0x7f.1/                 -> 127.0.0.1
http://017700000001/           -> 127.0.0.1
http://127.1/                  -> 127.0.0.1

Python 的 socket.inet_aton 行為與 RFC 警告的 C 函數庫一致,結果相同:

socket.inet_aton('2130706433')    -> 7f000001
socket.inet_aton('0x7f000001')    -> 7f000001
socket.inet_aton('017700000001')  -> 7f000001
socket.inet_aton('127.1')         -> 7f000001

而網絡層根本不在乎位址怎麼寫。把一個臨時 HTTP 伺服器綁在 127.0.0.1 的 8099 埠,每一種寫法都回傳 HTTP 200:

curl http://127.0.0.1:8099/       -> HTTP 200
curl http://2130706433:8099/      -> HTTP 200
curl http://0x7f.1:8099/          -> HTTP 200
curl http://017700000001:8099/    -> HTTP 200
curl http://127.1:8099/           -> HTTP 200

最後這一步才是重點。舊式寫法不是瀏覽器的奇觀,它會真的連上一個真的 socket。

05為什麼這是安全性問題

如果應用程式是靠比對 host 字串與清單來決定請求能不能通過,那編碼方式就是攻擊者決定的。只擋 127.0.0.1 與 localhost 的過濾器擋不住 2130706433;只擋 169.254.169.254 的過濾器擋不住 2852039166。

RFC 3986 在罕見位址那一節把風險講得很白:如果應用程式試圖用字串形式的 IP 位址來過濾存取,這些格式就會變成安全問題,而建議是先把字面值轉成數值,再用數字過濾。

這個模式出現在 SSRF 防禦、webhook 允許清單、圖片代理過濾與重導向驗證裡。封鎖清單的意圖通常是對的,實作卻是錯的,因為它把一個 32 位元數字當成字串。

字串封鎖清單不是位址封鎖清單。

127.0.0.1、127.1、2130706433、0x7f.1 與 017700000001 是同一個位址的五種編碼;169.254.169.254 與 2852039166 也是同一對。如果你的過濾器只認得每對裡的第一種寫法,它就是沒用。

06先正規化,再過濾

解法是加一個正規化步驟:把 host 轉成位址或整數,再拿那個值去比對規則。永遠不要比對原始字串。

嚴格解析器讓這件事更簡單,因為它們直接拒絕模糊的寫法。Python 的 ipaddress 模組接受點分十進位與整數輸入,但對舊式寫法會拋錯;Node 的 net.isIP 對它們全部回傳 0:

輸入Python ipaddress.ip_addressNode net.isIP
127.0.0.1127.0.0.14
2130706433ValueError0
0x7f.1ValueError0
017700000001ValueError0
127.1ValueError0

這個差異正是使用嚴格解析器的意義。沒通過嚴格檢查的值,不該再丟給 inet_aton 碰運氣;它應該成為拒絕這個請求的理由。

對既有程式碼來說,實務上的順序是:

  1. 用平台的 URL 解析器解析 URL,不要自己切字串。
  2. 取出解析後的 hostname,URL 標準已把它正規化成標準的 IPv4 字面值。
  3. 把這個字面值轉成整數或位址物件。
  4. 用數字比對允許清單與封鎖清單,涵蓋私有、loopback 與 link-local 範圍。
  5. 做完 DNS 解析後再檢查一次,因為一個網域名稱可以指向被封鎖的位址。

第 4 與第 5 步是多數 SSRF 防禦在實務上失敗的地方。正規化只在比較發生於標準形式時才有用,而只檢查 hostname、之後又重新解析的防線,等於沒有檢查。

07在各種表示法之間轉換

UrsaKit 的 進位轉換工具 是不寫程式就能看清各種表示法關係的最快方式。

  1. 把整數貼進 Value 欄位,例如 2130706433。
  2. 把 From base 設為 10。
  3. To base 留在 16,或切成 2、8 來看位元樣式。
  4. 按下 Convert,結果區會同時顯示二進位、八進位、十進位與十六進位。
  5. 複製十六進位值並兩兩切開:7f 00 00 01 就是 127.0.0.1。

這個工具對任意大小的整數都精確,不會像隨便找的計算機那樣把 32 位元值四捨五入成浮點數。如果你要檢查的是過濾器而不是值,正則表達式測試工具 會顯示既有樣式實際比對到什麼,而 URL 解碼工具 會在比對前解開百分比編碼——因為 %31%32%37... 又是同一串字元的另一種藏法。

症狀原因修正
封鎖清單漏掉 loopback 請求位址寫成整數或八進位先正規化成整數再比對
010.1.1.1 的行為不像 10.1.1.1URL 解析中開頭 0 代表八進位拒絕開頭 0,要求四個標準 octet
host 通過過濾,之後卻解析到別處過濾器檢查的是名稱而非解析後的位址先解析再過濾,並把連線綁在檢查過的位址
兩個日誌對同一次請求顯示不同 host每一層各自印出自己的正規化形式記錄過濾器實際使用的標準值

08資料來源

常見問題

什麼是 IPv4 十進位表示法?

就是把 IPv4 位址的 32 位元整數值直接寫出來、不加點。127.0.0.1 等於 2130706433,因為四個 octet 合起來就是一個 256 進位數字。

為什麼 2130706433 會連到 127.0.0.1?

URL 解析器支援一種舊式 IPv4 格式,允許用單一十進位數字代表整個 32 位元位址。瀏覽器與命令列工具沿用了這個行為,所以該 host 會解析成 127.0.0.1。

為什麼 017700000001 也代表 127.0.0.1?

舊式格式允許每一段使用 C 語言的數字前綴:開頭 0 代表八進位,開頭 0x 代表十六進位。017700000001 就是同一個 32 位元值的八進位寫法。

010.1.1.1 跟 10.1.1.1 一樣嗎?

不一樣。在舊式 URL 格式裡,開頭的 0 會讓該段變成八進位,所以 010 是 8,位址是 8.1.1.1。RFC 3986 的 dec-octet 文法根本不允許開頭有 0。

要怎麼安全地正規化 IP 位址?

把它解析成數字或位址物件,再比較數值。Python 的 ipaddress.ip_address 或 Node 的 net.isIP 這類嚴格解析器會直接拒絕舊式寫法,而 inet_aton 與 URL 解析器則會接受。

2852039166 是合法的 IP 位址嗎?

它是 169.254.169.254 的十進位形式,也就是雲端實例中介資料常用的 link-local 位址。任何只擋點分字串、沒擋整數的過濾器,都會讓這個端點保持可達。

為什麼瀏覽器還接受這些格式?

WHATWG URL 標準把這個行為寫成逐步算法,接受一到四段、每段可用十進位、八進位或十六進位。這是為了與瀏覽器歷史上解析 host 的方式保持網頁相容。

返回文章列表

相關文章

相關工具