3.5K Views
February 28, 23
スライド概要
Node学園 41時限目 LT
Node.js上でSSRF対策を実装する例
Node.jsとSSRF はせがわようすけ @hasegawayosuke
t #tng41
https://amazon.jp/dp/B0BQM1KMBG/ t #tng41
SSRF - Server Side Request Forgery サーバーから他のサーバーへリクエストを発行するときに、リクエ スト先を攻撃者が指定することができる脆弱性 内部ネットワーク上のサーバーへ間接的にアクセス可能になる 正 規 の 流 れ 攻 撃 の 流 れ http://example.jp/post? text = おもしろいニュース見つけたよ! & url=http://example.com/news 利用者 おもしろいニュース見つけたよ! http://example.com/news GET http://example.com/news example.comニュース 今日のニュースは○△□です。 example.jp example.com example.comニュース 今日のニュースは○△□です。 https://example.jp/post?url= https://intra.example.jp/news 攻撃者 [社外秘] 組織変更のお知らせ GET https://intra.example.jp/news example.jp [社外秘] 組織変更のお知らせ 内部ネットワーク上のホスト intra.example.jp t #tng41
SSRF - Server Side Request Forgery SSRF概要 サーバーから攻撃者が指定した他のサーバー/サービスへリクエストが飛ぶ攻撃 SSRFの脅威 機密情報の漏えい、他のプロトコルへの攻撃、任意 コード実行など 近隣でどんなサービスが動いているかに依る 詳細は「SSRF基礎」参照 https://www.docswell.com/s/hasegawa/VZGWQK-SSRF 対策 接続可能なURLをアプリケーション内で事前に定義 しておく iptables等でOS/コンテナレベルで接続先を制限 する URLの検証は保険的な対策にしかならない(やらない よりはマシ) 特にサーバー上の通信クライアントがcurl等のときはgopherのよ うな柔軟性の高いプロトコルが利用できてしまい危険性が上がる t #tng41
SSRFの対策は難しい ロジック自体の抜け漏れ:例 IPアドレス表記の多様性 → http://169.254.169.254/とかhttp://2852039166/とか http://0xa9fea9fe/とか DNSを使っての回避 169.254.169.254に名前を振る、短いTTLを充てる等 接続禁止先の抜け漏れ 内部ネットワークの構成変更等 今日は こっちだけ考える t #tng41
JSによる接続先の制限 接続先のIPアドレスやポートをNode上のコードで制限すること はできるか こういう感じのコードを実装したい if (接続先IPが内部アドレス or 接続先ポート番号が禁止ポート) { 接続をブロック } else { fetch(...) } fetchは細かな制御ができないので使えない → 標準のhttp.request で頑張る t #tng41
使えそうなもの: net.BlockList 接続禁止の仕組みになんかよさげなのが! https://nodejs.org/api/net.html#class-netblocklist リストを管理、照合するだけ。各APIに適用する方法はない! const list = new net.BlockList() list.addAddress('169.254.169.254') list.check(ipAddress) // true or false 照合結果を用いて、接続可否を自分で制御する必要がある t #tng41
使えそうなもの: http.requestのlookupオプション http.requestはDNSの名前解決をカスタマイズ可能 … snip … … snip … https://nodejs.org/api/http.html#httprequestoptions-callback t #tng41
実際に実装してみる bit.ly/tng41-ssrf t #tng41
const badAddresses = ['169.254.169.254']
const blockList = new net.BlockList()
badAddresses.forEach(address => {
blockList.addAddress(address)
})
Object.values(os.networkInterfaces()).forEach(nic => {
nic.forEach(o => {
const [network, prefix] = o.cidr.split(/¥//)
blockList.addSubnet(network, prefix | 0, o.family)
})
})
接続を禁止するIPアドレスの
リストを事前定義し
BlockListに登録する
ネットワークインタフェースを列
挙し、そのネットワークアドレス
を全て登録BlockListに登録
する
const _lookup = (hostname, options, callback) => {
const _callback = (err, address, family) => {
if (!err && blockList.check(address, `ipv${family}`)) {
throw new Error(`Invalid address: ${address}`)
}
callback.apply(this, [err, address, family])
}
dns.lookup.apply(this, [hostname, options, _callback])
}
DNSの名前解決でデフォ
ルトのlookupの前に
BlockListと照合し、合
致すれば例外
bit.ly/tng41-ssrf
const
const
const
const
objUrl = new URL(url)
isHttps = objUrl.protocol === 'https:'
port = objUrl.port ? objUrl.port | 0 : isHttps ? 443 : 80
badPorts = [0, 1, 7, 9, 11, ...]
接続禁止ポートはfetch standard参照
if (badPorts.includes(port)) {
接続先のポートが禁止リストに
throw new Error(`Invalid port: ${port}`)
合致すれば例外
}
if (net.isIP(objUrl.hostname)) {
ホスト名がIPアドレスのときに
if (blockList.check(objUrl.hostname)) {
禁止リストと合致するか検証
throw new Error(`Invalid address: ${objUrl.hostname}`)
(名前解決が走らないため)
}
}
const options = Object.create(null)
options.method = 'GET'
options.lookup = _lookup
lookupをカスタマイズしてrequest発行
const req = (isHttps ? https : http).request(url, options, (res) => {
...
})
bit.ly/tng41-ssrf
まとめ アプリケーションレイヤーで接続先を制限するのはめちゃくちゃめ んどくさい 先の実装でも何か抜け道ありそう fetch使えない、サードパーティのHTTPクライアントもたぶん使えない ➡ アプリケーションレイヤーだけでSSRF対策するのはかなり厳し い ネットワークレイヤーで制限するほうがたぶん簡単 多層防御という点で自分で実装してもいいけど、複雑化して別の問題が 発生しそう いい対策方法あれば教えて欲しい t #tng41
質問? ✉ t [email protected] @hasegawayosuke t #tng41