mirror of
https://github.com/wailsapp/wails.git
synced 2026-03-14 14:45:49 +01:00
parent
b7414a4303
commit
fb664a7c48
4 changed files with 321 additions and 2 deletions
|
|
@ -17,6 +17,7 @@ After processing, the content will be moved to the main changelog and this file
|
|||
|
||||
## Added
|
||||
<!-- New features, capabilities, or enhancements -->
|
||||
- Browser URL Sanitisation by @leaanthony in [#4500](https://github.dev/wailsapp/wails/pull/4500). Based on [#4484](https://github.com/wailsapp/wails/pull/4484) by @APShenkin.
|
||||
|
||||
## Changed
|
||||
<!-- Changes in existing functionality -->
|
||||
|
|
|
|||
|
|
@ -31,14 +31,21 @@ func (m *MessageProcessor) processBrowserMethod(method int, rw http.ResponseWrit
|
|||
return
|
||||
}
|
||||
|
||||
err := browser.OpenURL(*url)
|
||||
sanitizedURL, err := ValidateAndSanitizeURL(*url)
|
||||
if err != nil {
|
||||
m.Error("OpenURL: invalid URL - %s", err.Error())
|
||||
m.httpError(rw, fmt.Sprintf("Invalid URL: %s", err.Error()), err)
|
||||
return
|
||||
}
|
||||
|
||||
err = browser.OpenURL(sanitizedURL)
|
||||
if err != nil {
|
||||
m.httpError(rw, "OpenURL failed:", err)
|
||||
return
|
||||
}
|
||||
|
||||
m.ok(rw)
|
||||
m.Info("Runtime call:", "method", "Browser."+browserMethods[method], "url", *url)
|
||||
m.Info("Runtime call:", "method", "Browser."+browserMethods[method], "url", sanitizedURL)
|
||||
default:
|
||||
m.httpError(rw, "Invalid browser call:", fmt.Errorf("unknown method: %d", method))
|
||||
return
|
||||
|
|
|
|||
49
v3/pkg/application/urlvalidator.go
Normal file
49
v3/pkg/application/urlvalidator.go
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
package application
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"regexp"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func ValidateAndSanitizeURL(rawURL string) (string, error) {
|
||||
if strings.Contains(rawURL, "\x00") {
|
||||
return "", errors.New("null bytes not allowed in URL")
|
||||
}
|
||||
|
||||
for i, r := range rawURL {
|
||||
if r < 32 && r != 9 {
|
||||
return "", fmt.Errorf("control character at position %d not allowed", i)
|
||||
}
|
||||
}
|
||||
|
||||
shellDangerous := `[;|` + "`" + `$\\<>*{}\[\]()~! \t\n\r]`
|
||||
if matched, _ := regexp.MatchString(shellDangerous, rawURL); matched {
|
||||
return "", errors.New("shell metacharacters not allowed")
|
||||
}
|
||||
|
||||
unicodeDangerous := "[\u0000-\u001F\u007F\u00A0\u1680\u2000-\u200F\u2028-\u202F\u205F\u3000\uFEFF\u200B-\u200D\u2060\u2061\u2062\u2063\u2064\u206A-\u206F\uFFF0-\uFFFF]"
|
||||
if matched, _ := regexp.MatchString(unicodeDangerous, rawURL); matched {
|
||||
return "", errors.New("dangerous unicode characters not allowed")
|
||||
}
|
||||
|
||||
parsedURL, err := url.Parse(rawURL)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("invalid URL format: %v", err)
|
||||
}
|
||||
|
||||
scheme := strings.ToLower(parsedURL.Scheme)
|
||||
|
||||
if scheme == "javascript" || scheme == "data" || scheme == "file" || scheme == "ftp" || scheme == "" {
|
||||
return "", errors.New("scheme not allowed")
|
||||
}
|
||||
|
||||
if (scheme == "http" || scheme == "https") && parsedURL.Host == "" {
|
||||
return "", fmt.Errorf("missing host for %s URL", scheme)
|
||||
}
|
||||
|
||||
sanitizedURL := parsedURL.String()
|
||||
return sanitizedURL, nil
|
||||
}
|
||||
262
v3/pkg/application/urlvalidator_test.go
Normal file
262
v3/pkg/application/urlvalidator_test.go
Normal file
|
|
@ -0,0 +1,262 @@
|
|||
package application_test
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/wailsapp/wails/v3/pkg/application"
|
||||
)
|
||||
|
||||
func TestValidateURL(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
url string
|
||||
shouldErr bool
|
||||
errMsg string
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
name: "valid https URL",
|
||||
url: "https://www.example.com",
|
||||
shouldErr: false,
|
||||
expected: "https://www.example.com",
|
||||
},
|
||||
{
|
||||
name: "valid http URL",
|
||||
url: "http://example.com",
|
||||
shouldErr: false,
|
||||
expected: "http://example.com",
|
||||
},
|
||||
{
|
||||
name: "URL with query parameters",
|
||||
url: "https://example.com/search?q=cats&dogs",
|
||||
shouldErr: false,
|
||||
expected: "https://example.com/search?q=cats&dogs",
|
||||
},
|
||||
{
|
||||
name: "URL with port",
|
||||
url: "https://example.com:8080/path",
|
||||
shouldErr: false,
|
||||
expected: "https://example.com:8080/path",
|
||||
},
|
||||
{
|
||||
name: "URL with fragment",
|
||||
url: "https://example.com/page#section",
|
||||
shouldErr: false,
|
||||
expected: "https://example.com/page#section",
|
||||
},
|
||||
{
|
||||
name: "urlencode params",
|
||||
url: "http://google.com/ ----browser-subprocess-path=C:\\\\Users\\\\Public\\\\test.bat",
|
||||
shouldErr: true,
|
||||
errMsg: "shell metacharacters",
|
||||
},
|
||||
{
|
||||
name: "javascript scheme",
|
||||
url: "javascript:alert('XSS')",
|
||||
shouldErr: true,
|
||||
errMsg: "shell metacharacters",
|
||||
},
|
||||
{
|
||||
name: "data scheme",
|
||||
url: "data:text/html,<script>alert('XSS')</script>",
|
||||
shouldErr: true,
|
||||
errMsg: "shell metacharacters",
|
||||
},
|
||||
{
|
||||
name: "file scheme",
|
||||
url: "file:///etc/passwd",
|
||||
shouldErr: true,
|
||||
errMsg: "scheme not allowed",
|
||||
},
|
||||
{
|
||||
name: "ftp scheme",
|
||||
url: "ftp://ftp.example.com/file",
|
||||
shouldErr: true,
|
||||
errMsg: "scheme not allowed",
|
||||
},
|
||||
{
|
||||
name: "missing scheme",
|
||||
url: "example.com",
|
||||
shouldErr: true,
|
||||
errMsg: "scheme not allowed",
|
||||
},
|
||||
{
|
||||
name: "empty string",
|
||||
url: "",
|
||||
shouldErr: true,
|
||||
errMsg: "scheme not allowed",
|
||||
},
|
||||
{
|
||||
name: "null byte in URL",
|
||||
url: "https://example.com\x00/malicious",
|
||||
shouldErr: true,
|
||||
errMsg: "null bytes not allowed",
|
||||
},
|
||||
{
|
||||
name: "control character",
|
||||
url: "https://example.com\x01",
|
||||
shouldErr: true,
|
||||
errMsg: "control character",
|
||||
},
|
||||
{
|
||||
name: "shell injection with semicolon",
|
||||
url: "https://example.com/;rm -rf /",
|
||||
shouldErr: true,
|
||||
errMsg: "shell metacharacters",
|
||||
},
|
||||
{
|
||||
name: "shell injection with pipe",
|
||||
url: "https://example.com/|cat /etc/passwd",
|
||||
shouldErr: true,
|
||||
errMsg: "shell metacharacters",
|
||||
},
|
||||
{
|
||||
name: "shell injection with backtick",
|
||||
url: "https://example.com/`whoami`",
|
||||
shouldErr: true,
|
||||
errMsg: "shell metacharacters",
|
||||
},
|
||||
{
|
||||
name: "shell injection with dollar",
|
||||
url: "https://example.com/$(whoami)",
|
||||
shouldErr: true,
|
||||
errMsg: "shell metacharacters",
|
||||
},
|
||||
{
|
||||
name: "unicode null",
|
||||
url: "https://example.com/\u0000",
|
||||
shouldErr: true,
|
||||
errMsg: "null bytes not allowed",
|
||||
},
|
||||
{
|
||||
name: "missing host for http",
|
||||
url: "http:///path",
|
||||
shouldErr: true,
|
||||
errMsg: "missing host",
|
||||
},
|
||||
{
|
||||
name: "missing host for https",
|
||||
url: "https:///path",
|
||||
shouldErr: true,
|
||||
errMsg: "missing host",
|
||||
},
|
||||
{
|
||||
name: "URL with newline",
|
||||
url: "https://example.com/path\n/newline",
|
||||
shouldErr: true,
|
||||
errMsg: "control character",
|
||||
},
|
||||
{
|
||||
name: "URL with carriage return",
|
||||
url: "https://example.com/path\r/return",
|
||||
shouldErr: true,
|
||||
errMsg: "control character",
|
||||
},
|
||||
{
|
||||
name: "URL with tab",
|
||||
url: "https://example.com/path\t/tab",
|
||||
shouldErr: true,
|
||||
errMsg: "shell metacharacters",
|
||||
},
|
||||
{
|
||||
name: "URL with space in path",
|
||||
url: "https://example.com/path with spaces",
|
||||
shouldErr: true,
|
||||
errMsg: "shell metacharacters",
|
||||
},
|
||||
{
|
||||
name: "URL with angle brackets",
|
||||
url: "https://example.com/<script>",
|
||||
shouldErr: true,
|
||||
errMsg: "shell metacharacters",
|
||||
},
|
||||
{
|
||||
name: "URL with parentheses",
|
||||
url: "https://example.com/(test)",
|
||||
shouldErr: true,
|
||||
errMsg: "shell metacharacters",
|
||||
},
|
||||
{
|
||||
name: "URL with curly braces",
|
||||
url: "https://example.com/{test}",
|
||||
shouldErr: true,
|
||||
errMsg: "shell metacharacters",
|
||||
},
|
||||
{
|
||||
name: "URL with square brackets",
|
||||
url: "https://example.com/[test]",
|
||||
shouldErr: true,
|
||||
errMsg: "shell metacharacters",
|
||||
},
|
||||
{
|
||||
name: "URL with asterisk",
|
||||
url: "https://example.com/*",
|
||||
shouldErr: true,
|
||||
errMsg: "shell metacharacters",
|
||||
},
|
||||
{
|
||||
name: "URL with tilde",
|
||||
url: "https://example.com/~user",
|
||||
shouldErr: true,
|
||||
errMsg: "shell metacharacters",
|
||||
},
|
||||
{
|
||||
name: "URL with exclamation",
|
||||
url: "https://example.com/!test",
|
||||
shouldErr: true,
|
||||
errMsg: "shell metacharacters",
|
||||
},
|
||||
{
|
||||
name: "zero-width space",
|
||||
url: "https://example.com/\u200B",
|
||||
shouldErr: true,
|
||||
errMsg: "dangerous unicode",
|
||||
},
|
||||
{
|
||||
name: "zero-width non-joiner",
|
||||
url: "https://example.com/\u200C",
|
||||
shouldErr: true,
|
||||
errMsg: "dangerous unicode",
|
||||
},
|
||||
{
|
||||
name: "zero-width joiner",
|
||||
url: "https://example.com/\u200D",
|
||||
shouldErr: true,
|
||||
errMsg: "dangerous unicode",
|
||||
},
|
||||
{
|
||||
name: "right-to-left override",
|
||||
url: "https://example.com/\u202E",
|
||||
shouldErr: true,
|
||||
errMsg: "dangerous unicode",
|
||||
},
|
||||
{
|
||||
name: "invalid URL format",
|
||||
url: "ht!tp://[invalid",
|
||||
shouldErr: true,
|
||||
errMsg: "shell metacharacters",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
result, err := application.ValidateAndSanitizeURL(tc.url)
|
||||
|
||||
if tc.shouldErr {
|
||||
if err == nil {
|
||||
t.Errorf("expected error containing '%s', got nil", tc.errMsg)
|
||||
} else if !strings.Contains(err.Error(), tc.errMsg) {
|
||||
t.Errorf("expected error containing '%s', got '%s'", tc.errMsg, err.Error())
|
||||
}
|
||||
} else {
|
||||
if err != nil {
|
||||
t.Errorf("unexpected error: %v", err)
|
||||
}
|
||||
if result != tc.expected {
|
||||
t.Errorf("expected '%s', got '%s'", tc.expected, result)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue