Add comprehensive security improvements across the application: - **IP-based login protection**: Implement IP ban system tracking login failures, auto-banning after threshold exceeded - **Ownership verification**: Add userID parameter to Delete/Update operations for posts and comments to prevent unauthorized modifications - **SSRF protection**: Add URL and resolved host validation for image URLs in chat and OpenAI client - **SQL injection prevention**: Add EscapeLikeWildcard utility to escape special characters in LIKE queries - **HTTP security**: Configure server timeouts and add security headers middleware - **Rate limiting**: Refactor to support configurable duration and per-endpoint rate limits for auth routes - **Error handling**: Standardize error responses using HandleError and proper error types
135 lines
3.1 KiB
Go
135 lines
3.1 KiB
Go
package netutil
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"net"
|
|
"net/url"
|
|
"strings"
|
|
)
|
|
|
|
func isPrivateIP(ip net.IP) bool {
|
|
privateRanges := []struct {
|
|
network *net.IPNet
|
|
}{
|
|
{mustParseCIDR("10.0.0.0/8")},
|
|
{mustParseCIDR("172.16.0.0/12")},
|
|
{mustParseCIDR("192.168.0.0/16")},
|
|
{mustParseCIDR("127.0.0.0/8")},
|
|
{mustParseCIDR("169.254.0.0/16")},
|
|
{mustParseCIDR("0.0.0.0/8")},
|
|
{mustParseCIDR("100.64.0.0/10")},
|
|
{mustParseCIDR("198.18.0.0/15")},
|
|
{mustParseCIDR("::1/128")},
|
|
{mustParseCIDR("fc00::/7")},
|
|
{mustParseCIDR("fe80::/10")},
|
|
{mustParseCIDR("::ffff:127.0.0.0/104")},
|
|
}
|
|
|
|
for _, r := range privateRanges {
|
|
if r.network.Contains(ip) {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func mustParseCIDR(s string) *net.IPNet {
|
|
_, n, err := net.ParseCIDR(s)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
return n
|
|
}
|
|
|
|
type SafeDialer struct {
|
|
Dialer net.Dialer
|
|
}
|
|
|
|
func (d *SafeDialer) DialContext(ctx context.Context, network, addr string) (net.Conn, error) {
|
|
host, port, err := net.SplitHostPort(addr)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("netutil: invalid address %q: %w", addr, err)
|
|
}
|
|
|
|
resolver := net.Resolver{}
|
|
ips, err := resolver.LookupIPAddr(ctx, host)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("netutil: DNS lookup failed for %q: %w", host, err)
|
|
}
|
|
|
|
for _, ipAddr := range ips {
|
|
if isPrivateIP(ipAddr.IP) {
|
|
return nil, fmt.Errorf("netutil: resolved IP %s of %q is a private/reserved address", ipAddr.IP, host)
|
|
}
|
|
}
|
|
|
|
var targetIPs []string
|
|
for _, ipAddr := range ips {
|
|
targetIPs = append(targetIPs, ipAddr.IP.String())
|
|
}
|
|
|
|
var lastErr error
|
|
for _, ipStr := range targetIPs {
|
|
conn, err := d.Dialer.DialContext(ctx, network, net.JoinHostPort(ipStr, port))
|
|
if err != nil {
|
|
lastErr = err
|
|
continue
|
|
}
|
|
|
|
remoteAddr, ok := conn.RemoteAddr().(*net.TCPAddr)
|
|
if ok && isPrivateIP(remoteAddr.IP) {
|
|
conn.Close()
|
|
return nil, fmt.Errorf("netutil: connected IP %s is a private/reserved address (DNS rebinding detected)", remoteAddr.IP)
|
|
}
|
|
return conn, nil
|
|
}
|
|
|
|
if lastErr != nil {
|
|
return nil, lastErr
|
|
}
|
|
return nil, fmt.Errorf("netutil: no reachable IP for %q", host)
|
|
}
|
|
|
|
func ValidateURL(rawURL string) error {
|
|
u, err := url.Parse(rawURL)
|
|
if err != nil {
|
|
return fmt.Errorf("netutil: invalid URL: %w", err)
|
|
}
|
|
|
|
scheme := strings.ToLower(u.Scheme)
|
|
if scheme != "http" && scheme != "https" {
|
|
return fmt.Errorf("netutil: scheme %q is not allowed, only http and https are permitted", u.Scheme)
|
|
}
|
|
|
|
if u.Host == "" {
|
|
return fmt.Errorf("netutil: URL missing host")
|
|
}
|
|
|
|
hostname := u.Hostname()
|
|
if hostname == "" {
|
|
return fmt.Errorf("netutil: URL missing hostname")
|
|
}
|
|
|
|
if ip := net.ParseIP(hostname); ip != nil {
|
|
if isPrivateIP(ip) {
|
|
return fmt.Errorf("netutil: host IP %s is a private/reserved address", ip)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func CheckResolvedHost(hostname string) error {
|
|
ips, err := net.LookupIP(hostname)
|
|
if err != nil {
|
|
return fmt.Errorf("netutil: DNS lookup failed for %q: %w", hostname, err)
|
|
}
|
|
for _, ip := range ips {
|
|
if isPrivateIP(ip) {
|
|
return fmt.Errorf("netutil: resolved IP %s of %q is a private/reserved address", ip, hostname)
|
|
}
|
|
}
|
|
return nil
|
|
}
|