Skip to content

Commit c5fd25f

Browse files
authored
Merge pull request #2902 from traPtitech/hotfix/ogp-ssrf
fix SSRF when fetching OGP
2 parents 55e8c46 + 4bd6419 commit c5fd25f

2 files changed

Lines changed: 97 additions & 1 deletion

File tree

service/ogp/parser/errors.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,4 +15,6 @@ var (
1515
ErrServer = errors.New("network error (server)")
1616
// ErrDomainRequest 特殊処理を行うドメインのURLが期待した形式ではありませんでした
1717
ErrDomainRequest = errors.New("bad request for special domain ")
18+
// ErrNotAllowed URLが許可されていません
19+
ErrNotAllowed = errors.New("access to this URL is not allowed")
1820
)

service/ogp/parser/parser.go

Lines changed: 95 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package parser
22

33
import (
44
"context"
5+
"net"
56
"net/http"
67
"net/url"
78
"strings"
@@ -21,19 +22,112 @@ type DefaultPageMeta struct {
2122
Title, Description, URL, Image string
2223
}
2324

25+
// isPrivateIP はIPアドレスがプライベート、ループバック、リンクローカル、またはその他の内部アドレスかどうかを判定します
26+
func isPrivateIP(ip net.IP) bool {
27+
if ip == nil {
28+
return true // 不明なIPはブロック
29+
}
30+
// ループバック (127.0.0.0/8, ::1)
31+
if ip.IsLoopback() {
32+
return true
33+
}
34+
// プライベートアドレス (10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16, fc00::/7)
35+
if ip.IsPrivate() {
36+
return true
37+
}
38+
// リンクローカル (169.254.0.0/16, fe80::/10)
39+
if ip.IsLinkLocalUnicast() || ip.IsLinkLocalMulticast() {
40+
return true
41+
}
42+
// 未指定アドレス (0.0.0.0, ::)
43+
if ip.IsUnspecified() {
44+
return true
45+
}
46+
// マルチキャスト
47+
if ip.IsMulticast() {
48+
return true
49+
}
50+
return false
51+
}
52+
53+
// validateURL はURLがSSRFに対して安全かどうかを検証し、検証済みのIPアドレスを返します
54+
func validateURL(u *url.URL) ([]net.IP, error) {
55+
// スキームの検証
56+
if u.Scheme != "http" && u.Scheme != "https" {
57+
return nil, ErrNotAllowed
58+
}
59+
60+
// ホスト名を取得
61+
host := u.Hostname()
62+
if host == "" {
63+
return nil, ErrNotAllowed
64+
}
65+
66+
// IPアドレスの場合は直接検証
67+
if ip := net.ParseIP(host); ip != nil {
68+
if isPrivateIP(ip) {
69+
return nil, ErrNotAllowed
70+
}
71+
return []net.IP{ip}, nil
72+
}
73+
74+
// ホスト名の場合はDNS解決して検証
75+
ips, err := net.LookupIP(host)
76+
if err != nil {
77+
return nil, ErrNetwork
78+
}
79+
80+
for _, ip := range ips {
81+
if isPrivateIP(ip) {
82+
return nil, ErrNotAllowed
83+
}
84+
}
85+
86+
return ips, nil
87+
}
88+
2489
// ParseMetaForURL 指定したURLのメタタグをパースした結果を返します。
2590
func ParseMetaForURL(url *url.URL) (*opengraph.OpenGraph, *DefaultPageMeta, error) {
2691
_ = requestLimiter.Acquire(context.Background(), 1)
2792
defer requestLimiter.Release(1)
2893

94+
// SSRF対策: URLを検証し、検証済みIPアドレスを取得
95+
resolvedIPs, err := validateURL(url)
96+
if err != nil {
97+
return nil, nil, err
98+
}
99+
29100
og, meta, isSpecialDomain, err := FetchSpecialDomainInfo(url)
30101
if isSpecialDomain && (err == nil) {
31102
return og, meta, nil
32103
}
33104

34-
client := http.Client{
105+
// SSRF対策: 検証済みのIPアドレスを使用してリクエストを送信
106+
dialer := &net.Dialer{
35107
Timeout: 5 * time.Second,
36108
}
109+
transport := &http.Transport{
110+
DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) {
111+
_, port, err := net.SplitHostPort(addr)
112+
if err != nil {
113+
return nil, err
114+
}
115+
116+
// 検証済みのIPアドレスに接続
117+
addr = net.JoinHostPort(resolvedIPs[0].String(), port)
118+
return dialer.DialContext(ctx, network, addr)
119+
},
120+
}
121+
122+
client := http.Client{
123+
Timeout: 5 * time.Second,
124+
Transport: transport,
125+
CheckRedirect: func(req *http.Request, _ []*http.Request) error {
126+
// Validate redirect destination to prevent SSRF via redirects
127+
_, err := validateURL(req.URL)
128+
return err
129+
},
130+
}
37131

38132
req, err := http.NewRequest("GET", url.String(), nil)
39133
if err != nil {

0 commit comments

Comments
 (0)