Some checks failed
Pipeline: Test, Lint, Build / Get version info (push) Has been cancelled
Pipeline: Test, Lint, Build / Lint Go code (push) Has been cancelled
Pipeline: Test, Lint, Build / Test Go code (push) Has been cancelled
Pipeline: Test, Lint, Build / Test JS code (push) Has been cancelled
Pipeline: Test, Lint, Build / Lint i18n files (push) Has been cancelled
Pipeline: Test, Lint, Build / Check Docker configuration (push) Has been cancelled
Pipeline: Test, Lint, Build / Build (darwin/amd64) (push) Has been cancelled
Pipeline: Test, Lint, Build / Build (darwin/arm64) (push) Has been cancelled
Pipeline: Test, Lint, Build / Build (linux/386) (push) Has been cancelled
Pipeline: Test, Lint, Build / Build (linux/amd64) (push) Has been cancelled
Pipeline: Test, Lint, Build / Build (linux/arm/v5) (push) Has been cancelled
Pipeline: Test, Lint, Build / Build (linux/arm/v6) (push) Has been cancelled
Pipeline: Test, Lint, Build / Build (linux/arm/v7) (push) Has been cancelled
Pipeline: Test, Lint, Build / Build (linux/arm64) (push) Has been cancelled
Pipeline: Test, Lint, Build / Build (windows/386) (push) Has been cancelled
Pipeline: Test, Lint, Build / Build (windows/amd64) (push) Has been cancelled
Pipeline: Test, Lint, Build / Push to GHCR (push) Has been cancelled
Pipeline: Test, Lint, Build / Push to Docker Hub (push) Has been cancelled
Pipeline: Test, Lint, Build / Cleanup digest artifacts (push) Has been cancelled
Pipeline: Test, Lint, Build / Build Windows installers (push) Has been cancelled
Pipeline: Test, Lint, Build / Package/Release (push) Has been cancelled
Pipeline: Test, Lint, Build / Upload Linux PKG (push) Has been cancelled
Close stale issues and PRs / stale (push) Has been cancelled
POEditor import / update-translations (push) Has been cancelled
193 lines
4.8 KiB
Go
193 lines
4.8 KiB
Go
package plugins
|
|
|
|
import (
|
|
"fmt"
|
|
"net"
|
|
"net/url"
|
|
"regexp"
|
|
"strings"
|
|
)
|
|
|
|
// NetworkPermissionsBase contains common functionality for network-based permissions
|
|
type networkPermissionsBase struct {
|
|
Reason string `json:"reason"`
|
|
AllowLocalNetwork bool `json:"allowLocalNetwork,omitempty"`
|
|
}
|
|
|
|
// URLMatcher provides URL pattern matching functionality
|
|
type urlMatcher struct{}
|
|
|
|
// newURLMatcher creates a new URL matcher instance
|
|
func newURLMatcher() *urlMatcher {
|
|
return &urlMatcher{}
|
|
}
|
|
|
|
// checkURLPolicy performs common checks for a URL against network policies.
|
|
func checkURLPolicy(requestURL string, allowLocalNetwork bool) (*url.URL, error) {
|
|
parsedURL, err := url.Parse(requestURL)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("invalid URL: %w", err)
|
|
}
|
|
|
|
// Check local network restrictions
|
|
if !allowLocalNetwork {
|
|
if err := checkLocalNetwork(parsedURL); err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
return parsedURL, nil
|
|
}
|
|
|
|
// MatchesURLPattern checks if a URL matches a given pattern
|
|
func (m *urlMatcher) MatchesURLPattern(requestURL, pattern string) bool {
|
|
// Handle wildcard pattern
|
|
if pattern == "*" {
|
|
return true
|
|
}
|
|
|
|
// Parse both URLs to handle path matching correctly
|
|
reqURL, err := url.Parse(requestURL)
|
|
if err != nil {
|
|
return false
|
|
}
|
|
|
|
patternURL, err := url.Parse(pattern)
|
|
if err != nil {
|
|
// If pattern is not a valid URL, treat it as a simple string pattern
|
|
regexPattern := m.urlPatternToRegex(pattern)
|
|
matched, err := regexp.MatchString(regexPattern, requestURL)
|
|
if err != nil {
|
|
return false
|
|
}
|
|
return matched
|
|
}
|
|
|
|
// Match scheme
|
|
if patternURL.Scheme != "" && patternURL.Scheme != reqURL.Scheme {
|
|
return false
|
|
}
|
|
|
|
// Match host with wildcard support
|
|
if !m.matchesHost(reqURL.Host, patternURL.Host) {
|
|
return false
|
|
}
|
|
|
|
// Match path with wildcard support
|
|
// Special case: if pattern URL has empty path and contains wildcards, allow any path (domain-only wildcard matching)
|
|
if (patternURL.Path == "" || patternURL.Path == "/") && strings.Contains(pattern, "*") {
|
|
// This is a domain-only wildcard pattern, allow any path
|
|
return true
|
|
}
|
|
if !m.matchesPath(reqURL.Path, patternURL.Path) {
|
|
return false
|
|
}
|
|
|
|
return true
|
|
}
|
|
|
|
// urlPatternToRegex converts a URL pattern with wildcards to a regex pattern
|
|
func (m *urlMatcher) urlPatternToRegex(pattern string) string {
|
|
// Escape special regex characters except *
|
|
escaped := regexp.QuoteMeta(pattern)
|
|
|
|
// Replace escaped \* with regex pattern for wildcard matching
|
|
// For subdomain: *.example.com -> [^.]*\.example\.com
|
|
// For path: /api/* -> /api/.*
|
|
escaped = strings.ReplaceAll(escaped, "\\*", ".*")
|
|
|
|
// Anchor the pattern to match the full URL
|
|
return "^" + escaped + "$"
|
|
}
|
|
|
|
// matchesHost checks if a host matches a pattern with wildcard support
|
|
func (m *urlMatcher) matchesHost(host, pattern string) bool {
|
|
if pattern == "" {
|
|
return true
|
|
}
|
|
|
|
if pattern == "*" {
|
|
return true
|
|
}
|
|
|
|
// Handle wildcard patterns anywhere in the host
|
|
if strings.Contains(pattern, "*") {
|
|
patterns := []string{
|
|
strings.ReplaceAll(regexp.QuoteMeta(pattern), "\\*", "[0-9.]+"), // IP pattern
|
|
strings.ReplaceAll(regexp.QuoteMeta(pattern), "\\*", "[^.]*"), // Domain pattern
|
|
}
|
|
|
|
for _, regexPattern := range patterns {
|
|
fullPattern := "^" + regexPattern + "$"
|
|
if matched, err := regexp.MatchString(fullPattern, host); err == nil && matched {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
return host == pattern
|
|
}
|
|
|
|
// matchesPath checks if a path matches a pattern with wildcard support
|
|
func (m *urlMatcher) matchesPath(path, pattern string) bool {
|
|
// Normalize empty paths to "/"
|
|
if path == "" {
|
|
path = "/"
|
|
}
|
|
if pattern == "" {
|
|
pattern = "/"
|
|
}
|
|
|
|
if pattern == "*" {
|
|
return true
|
|
}
|
|
|
|
// Handle wildcard paths
|
|
if strings.HasSuffix(pattern, "/*") {
|
|
prefix := pattern[:len(pattern)-2] // Remove "/*"
|
|
if prefix == "" {
|
|
prefix = "/"
|
|
}
|
|
return strings.HasPrefix(path, prefix)
|
|
}
|
|
|
|
return path == pattern
|
|
}
|
|
|
|
// CheckLocalNetwork checks if the URL is accessing local network resources
|
|
func checkLocalNetwork(parsedURL *url.URL) error {
|
|
host := parsedURL.Hostname()
|
|
|
|
// Check for localhost variants
|
|
if host == "localhost" || host == "127.0.0.1" || host == "::1" {
|
|
return fmt.Errorf("requests to localhost are not allowed")
|
|
}
|
|
|
|
// Try to parse as IP address
|
|
ip := net.ParseIP(host)
|
|
if ip != nil && isPrivateIP(ip) {
|
|
return fmt.Errorf("requests to private IP addresses are not allowed")
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// IsPrivateIP checks if an IP is loopback, private, or link-local (IPv4/IPv6).
|
|
func isPrivateIP(ip net.IP) bool {
|
|
if ip == nil {
|
|
return false
|
|
}
|
|
if ip.IsLoopback() || ip.IsPrivate() {
|
|
return true
|
|
}
|
|
// IPv4 link-local: 169.254.0.0/16
|
|
if ip4 := ip.To4(); ip4 != nil {
|
|
return ip4[0] == 169 && ip4[1] == 254
|
|
}
|
|
// IPv6 link-local: fe80::/10
|
|
if ip16 := ip.To16(); ip16 != nil && ip.To4() == nil {
|
|
return ip16[0] == 0xfe && (ip16[1]&0xc0) == 0x80
|
|
}
|
|
return false
|
|
}
|