[V2] Add origin verification for bindings (#4480)

* Update go-webview2 to v1.0.22

---------

Co-authored-by: Lea Anthony <lea.anthony@gmail.com>
This commit is contained in:
Andrey Pshenkin 2025-08-13 01:50:23 +03:00 committed by GitHub
commit 857f7b7518
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 318 additions and 27 deletions

View file

@ -37,7 +37,7 @@ require (
github.com/tc-hib/winres v0.3.1
github.com/tidwall/sjson v1.2.5
github.com/tkrajina/go-reflector v0.5.8
github.com/wailsapp/go-webview2 v1.0.19
github.com/wailsapp/go-webview2 v1.0.22
github.com/wailsapp/mimetype v1.4.1
github.com/wzshiming/ctc v1.2.3
golang.org/x/mod v0.23.0

View file

@ -76,10 +76,6 @@ github.com/flytam/filenamify v1.2.0 h1:7RiSqXYR4cJftDQ5NuvljKMfd/ubKnW/j9C6iekCh
github.com/flytam/filenamify v1.2.0/go.mod h1:Dzf9kVycwcsBlr2ATg6uxjqiFgKGH+5SKFuhdeP5zu8=
github.com/frankban/quicktest v1.14.5 h1:dfYrrRyLtiqT9GyKXgdh+k4inNeTvmGbuSgZ3lx3GhA=
github.com/frankban/quicktest v1.14.5/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4=
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
github.com/fsnotify/fsnotify v1.8.0 h1:dAwr6QBTBZIkG8roQaJjGof0pp0EeF+tNV7YBP3F/8M=
github.com/fsnotify/fsnotify v1.8.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
github.com/gliderlabs/ssh v0.3.8 h1:a4YXD1V7xMF9g5nTkdfnja3Sxy1PVDCj1Zg4Wb8vY6c=
@ -248,8 +244,8 @@ github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6Kllzaw
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
github.com/valyala/fasttemplate v1.2.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQD0Loo=
github.com/valyala/fasttemplate v1.2.2/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ=
github.com/wailsapp/go-webview2 v1.0.19 h1:7U3QcDj1PrBPaxJNCui2k1SkWml+Q5kvFUFyTImA6NU=
github.com/wailsapp/go-webview2 v1.0.19/go.mod h1:qJmWAmAmaniuKGZPWwne+uor3AHMB5PFhqiK0Bbj8kc=
github.com/wailsapp/go-webview2 v1.0.22 h1:YT61F5lj+GGaat5OB96Aa3b4QA+mybD0Ggq6NZijQ58=
github.com/wailsapp/go-webview2 v1.0.22/go.mod h1:qJmWAmAmaniuKGZPWwne+uor3AHMB5PFhqiK0Bbj8kc=
github.com/wailsapp/mimetype v1.4.1 h1:pQN9ycO7uo4vsUUuPeHEYoUkLVkaRntMnHJxVwYhwHs=
github.com/wailsapp/mimetype v1.4.1/go.mod h1:9aV5k31bBOv5z6u+QP8TltzvNGJPmNJD4XlAL3U+j3o=
github.com/wzshiming/ctc v1.2.3 h1:q+hW3IQNsjIlOFBTGZZZeIXTElFM4grF4spW/errh/c=
@ -296,7 +292,6 @@ golang.org/x/sync v0.11.0 h1:GGz8+XQP4FvTTrjZPzNKTMFtSXH80RAzG+5ghFPgK9w=
golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200107162124-548cf772de50/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200810151505-1b9f1253b3ed/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=

View file

@ -477,6 +477,15 @@ typedef void (^schemeTaskCaller)(id<WKURLSchemeTask>);
}
- (void)userContentController:(nonnull WKUserContentController *)userContentController didReceiveScriptMessage:(nonnull WKScriptMessage *)message {
// Get the origin from the message's frame
NSString *origin = nil;
if (message.frameInfo && message.frameInfo.request && message.frameInfo.request.URL) {
NSURL *url = message.frameInfo.request.URL;
if (url.scheme && url.host) {
origin = [url absoluteString];
}
}
NSString *m = message.body;
// Check for drag
@ -491,11 +500,11 @@ typedef void (^schemeTaskCaller)(id<WKURLSchemeTask>);
}
const char *_m = [m UTF8String];
const char *_origin = [origin UTF8String];
processMessage(_m);
processBindingMessage(_m, _origin, message.frameInfo.isMainFrame);
}
/***** Dialogs ******/
-(void) MessageDialog :(NSString*)dialogType :(NSString*)title :(NSString*)message :(NSString*)button1 :(NSString*)button2 :(NSString*)button3 :(NSString*)button4 :(NSString*)defaultButton :(NSString*)cancelButton :(void*)iconData :(int)iconDataLength {

View file

@ -31,6 +31,7 @@ import (
"github.com/wailsapp/wails/v2/internal/binding"
"github.com/wailsapp/wails/v2/internal/frontend"
"github.com/wailsapp/wails/v2/internal/frontend/originvalidator"
"github.com/wailsapp/wails/v2/internal/frontend/runtime"
"github.com/wailsapp/wails/v2/internal/logger"
"github.com/wailsapp/wails/v2/pkg/options"
@ -38,13 +39,20 @@ import (
const startURL = "wails://wails/"
type bindingsMessage struct {
message string
source string
isMainFrame bool
}
var (
messageBuffer = make(chan string, 100)
requestBuffer = make(chan webview.Request, 100)
callbackBuffer = make(chan uint, 10)
openFilepathBuffer = make(chan string, 100)
openUrlBuffer = make(chan string, 100)
secondInstanceBuffer = make(chan options.SecondInstanceData, 1)
messageBuffer = make(chan string, 100)
bindingsMessageBuffer = make(chan *bindingsMessage, 100)
requestBuffer = make(chan webview.Request, 100)
callbackBuffer = make(chan uint, 10)
openFilepathBuffer = make(chan string, 100)
openUrlBuffer = make(chan string, 100)
secondInstanceBuffer = make(chan options.SecondInstanceData, 1)
)
type Frontend struct {
@ -67,6 +75,8 @@ type Frontend struct {
mainWindow *Window
bindings *binding.Bindings
dispatcher frontend.Dispatcher
originValidator *originvalidator.OriginValidator
}
func (f *Frontend) RunMainLoop() {
@ -86,15 +96,18 @@ func NewFrontend(ctx context.Context, appoptions *options.App, myLogger *logger.
ctx: ctx,
}
result.startURL, _ = url.Parse(startURL)
result.originValidator = originvalidator.NewOriginValidator(result.startURL, appoptions.BindingsAllowedOrigins)
// this should be initialized as early as possible to handle first instance launch
C.StartCustomProtocolHandler()
if _starturl, _ := ctx.Value("starturl").(*url.URL); _starturl != nil {
result.startURL = _starturl
result.originValidator = originvalidator.NewOriginValidator(result.startURL, appoptions.BindingsAllowedOrigins)
} else {
if port, _ := ctx.Value("assetserverport").(string); port != "" {
result.startURL.Host = net.JoinHostPort(result.startURL.Host+".localhost", port)
result.originValidator = originvalidator.NewOriginValidator(result.startURL, appoptions.BindingsAllowedOrigins)
}
var bindings string
@ -119,6 +132,7 @@ func NewFrontend(ctx context.Context, appoptions *options.App, myLogger *logger.
}
go result.startMessageProcessor()
go result.startBindingsMessageProcessor()
go result.startCallbackProcessor()
go result.startFileOpenProcessor()
go result.startUrlOpenProcessor()
@ -154,6 +168,30 @@ func (f *Frontend) startMessageProcessor() {
}
}
func (f *Frontend) startBindingsMessageProcessor() {
for msg := range bindingsMessageBuffer {
// Apple webkit doesn't provide origin of main frame. So we can't verify in case of iFrame that top level origin is allowed.
if !msg.isMainFrame {
f.logger.Error("Blocked request from not main frame")
continue
}
origin, err := f.originValidator.GetOriginFromURL(msg.source)
if err != nil {
f.logger.Error(fmt.Sprintf("failed to get origin for URL %q: %v", msg.source, err))
continue
}
allowed := f.originValidator.IsOriginAllowed(origin)
if !allowed {
f.logger.Error("Blocked request from unauthorized origin: %s", origin)
continue
}
f.processMessage(msg.message)
}
}
func (f *Frontend) startRequestProcessor() {
for request := range requestBuffer {
f.assets.ServeWebViewRequest(request)
@ -453,6 +491,17 @@ func processMessage(message *C.char) {
messageBuffer <- goMessage
}
//export processBindingMessage
func processBindingMessage(message *C.char, source *C.char, fromMainFrame bool) {
goMessage := C.GoString(message)
goSource := C.GoString(source)
bindingsMessageBuffer <- &bindingsMessage{
message: goMessage,
source: goSource,
isMainFrame: fromMainFrame,
}
}
//export processCallback
func processCallback(callbackID uint) {
callbackBuffer <- callbackID

View file

@ -15,6 +15,7 @@ extern "C"
#endif
void processMessage(const char *);
void processBindingMessage(const char *, const char *, bool);
void processURLRequest(void *, void*);
void processMessageDialogResponse(int);
void processOpenFileDialogResponse(const char*);

View file

@ -4,7 +4,7 @@
package linux
/*
#cgo linux pkg-config: gtk+-3.0
#cgo linux pkg-config: gtk+-3.0
#cgo !webkit2_41 pkg-config: webkit2gtk-4.0
#cgo webkit2_41 pkg-config: webkit2gtk-4.1
@ -95,6 +95,7 @@ import (
"github.com/wailsapp/wails/v2/internal/binding"
"github.com/wailsapp/wails/v2/internal/frontend"
"github.com/wailsapp/wails/v2/internal/frontend/originvalidator"
wailsruntime "github.com/wailsapp/wails/v2/internal/frontend/runtime"
"github.com/wailsapp/wails/v2/internal/logger"
"github.com/wailsapp/wails/v2/pkg/options"
@ -124,6 +125,8 @@ type Frontend struct {
mainWindow *Window
bindings *binding.Bindings
dispatcher frontend.Dispatcher
originValidator *originvalidator.OriginValidator
}
func (f *Frontend) RunMainLoop() {
@ -156,12 +159,15 @@ func NewFrontend(ctx context.Context, appoptions *options.App, myLogger *logger.
ctx: ctx,
}
result.startURL, _ = url.Parse(startURL)
result.originValidator = originvalidator.NewOriginValidator(result.startURL, appoptions.BindingsAllowedOrigins)
if _starturl, _ := ctx.Value("starturl").(*url.URL); _starturl != nil {
result.startURL = _starturl
result.originValidator = originvalidator.NewOriginValidator(result.startURL, appoptions.BindingsAllowedOrigins)
} else {
if port, _ := ctx.Value("assetserverport").(string); port != "" {
result.startURL.Host = net.JoinHostPort(result.startURL.Host+".localhost", port)
result.originValidator = originvalidator.NewOriginValidator(result.startURL, appoptions.BindingsAllowedOrigins)
}
var bindings string
@ -184,6 +190,7 @@ func NewFrontend(ctx context.Context, appoptions *options.App, myLogger *logger.
}
go result.startMessageProcessor()
go result.startBindingsMessageProcessor()
var _debug = ctx.Value("debug")
var _devtoolsEnabled = ctx.Value("devtoolsEnabled")
@ -216,6 +223,24 @@ func (f *Frontend) startMessageProcessor() {
}
}
func (f *Frontend) startBindingsMessageProcessor() {
for msg := range bindingsMessageBuffer {
origin, err := f.originValidator.GetOriginFromURL(msg.source)
if err != nil {
f.logger.Error(fmt.Sprintf("failed to get origin for URL %q: %v", msg.source, err))
continue
}
allowed := f.originValidator.IsOriginAllowed(origin)
if !allowed {
f.logger.Error("Blocked request from unauthorized origin: %s", origin)
continue
}
f.processMessage(msg.message)
}
}
func (f *Frontend) WindowReload() {
f.ExecJS("runtime.WindowReload();")
}
@ -507,7 +532,13 @@ func (f *Frontend) ExecJS(js string) {
f.mainWindow.ExecJS(js)
}
type bindingsMessage struct {
message string
source string
}
var messageBuffer = make(chan string, 100)
var bindingsMessageBuffer = make(chan *bindingsMessage, 100)
//export processMessage
func processMessage(message *C.char) {
@ -515,6 +546,16 @@ func processMessage(message *C.char) {
messageBuffer <- goMessage
}
//export processBindingMessage
func processBindingMessage(message *C.char, source *C.char) {
goMessage := C.GoString(message)
goSource := C.GoString(source)
bindingsMessageBuffer <- &bindingsMessage{
message: goMessage,
source: goSource,
}
}
var requestBuffer = make(chan webview.Request, 100)
func (f *Frontend) startRequestProcessor() {

View file

@ -45,11 +45,17 @@ GtkBox *GTKBOX(void *pointer)
}
extern void processMessage(char *);
extern void processBindingMessage(char *, char *);
static void sendMessageToBackend(WebKitUserContentManager *contentManager,
WebKitJavascriptResult *result,
void *data)
{
// Retrieve webview from content manager
WebKitWebView *webview = WEBKIT_WEB_VIEW(g_object_get_data(G_OBJECT(contentManager), "webview"));
const char *current_uri = webview ? webkit_web_view_get_uri(webview) : NULL;
char *uri = current_uri ? g_strdup(current_uri) : NULL;
#if WEBKIT_MAJOR_VERSION >= 2 && WEBKIT_MINOR_VERSION >= 22
JSCValue *value = webkit_javascript_result_get_js_value(result);
char *message = jsc_value_to_string(value);
@ -62,8 +68,11 @@ static void sendMessageToBackend(WebKitUserContentManager *contentManager,
JSStringGetUTF8CString(js, message, messageSize);
JSStringRelease(js);
#endif
processMessage(message);
processBindingMessage(message, uri);
g_free(message);
if (uri) {
g_free(uri);
}
}
static bool isNULLRectangle(GdkRectangle input)
@ -78,7 +87,7 @@ static gboolean onWayland()
case -1:
{
char *gdkBackend = getenv("XDG_SESSION_TYPE");
if(gdkBackend != NULL && strcmp(gdkBackend, "wayland") == 0)
if(gdkBackend != NULL && strcmp(gdkBackend, "wayland") == 0)
{
wmIsWayland = 1;
return TRUE;
@ -273,7 +282,7 @@ void SetMinMaxSize(GtkWindow *window, int min_width, int min_height, int max_wid
size.min_width = min_width;
// On Wayland window manager get the decorators and calculate the differences from the windows' size.
if(onWayland())
if(onWayland())
{
if(decoratorWidth == -1 && decoratorHeight == -1)
{
@ -284,9 +293,9 @@ void SetMinMaxSize(GtkWindow *window, int min_width, int min_height, int max_wid
gtk_widget_get_allocation(GTK_WIDGET(window), &windowAllocation);
decoratorWidth = (windowAllocation.width-windowWidth);
decoratorHeight = (windowAllocation.height-windowHeight);
decoratorHeight = (windowAllocation.height-windowHeight);
}
// Add the decorator difference to the window so fullscreen and maximise can fill the window.
size.max_height = decoratorHeight+size.max_height;
size.max_width = decoratorWidth+size.max_width;
@ -549,6 +558,9 @@ static gboolean onDragDrop(GtkWidget* self, GdkDragContext* context, gint x, gin
GtkWidget *SetupWebview(void *contentManager, GtkWindow *window, int hideWindowOnClose, int gpuPolicy, int disableWebViewDragAndDrop, int enableDragAndDrop)
{
GtkWidget *webview = webkit_web_view_new_with_user_content_manager((WebKitUserContentManager *)contentManager);
// Store webview reference in the content manager
g_object_set_data(G_OBJECT((WebKitUserContentManager *)contentManager), "webview", webview);
// gtk_container_add(GTK_CONTAINER(window), webview);
WebKitWebContext *context = webkit_web_context_get_default();
webkit_web_context_register_uri_scheme(context, "wails", (WebKitURISchemeRequestCallback)processURLRequest, NULL, NULL);
@ -876,4 +888,4 @@ void InstallF12Hotkey(void *window)
gtk_window_add_accel_group(GTK_WINDOW(window), accel_group);
GClosure *closure = g_cclosure_new(G_CALLBACK(sendShowInspectorMessage), window, NULL);
gtk_accel_group_connect(accel_group, GDK_KEY_F12, GDK_CONTROL_MASK | GDK_SHIFT_MASK, GTK_ACCEL_VISIBLE, closure);
}
}

View file

@ -25,6 +25,7 @@ import (
"github.com/wailsapp/wails/v2/internal/frontend/desktop/windows/win32"
"github.com/wailsapp/wails/v2/internal/frontend/desktop/windows/winc"
"github.com/wailsapp/wails/v2/internal/frontend/desktop/windows/winc/w32"
"github.com/wailsapp/wails/v2/internal/frontend/originvalidator"
wailsruntime "github.com/wailsapp/wails/v2/internal/frontend/runtime"
"github.com/wailsapp/wails/v2/internal/logger"
"github.com/wailsapp/wails/v2/internal/system/operatingsystem"
@ -62,6 +63,8 @@ type Frontend struct {
hasStarted bool
originValidator *originvalidator.OriginValidator
// Windows build number
versionInfo *operatingsystem.WindowsVersionInfo
resizeDebouncer func(f func())
@ -89,14 +92,17 @@ func NewFrontend(ctx context.Context, appoptions *options.App, myLogger *logger.
// We currently can't use wails://wails/ as other platforms do, therefore we map the assets sever onto the following url.
result.startURL, _ = url.Parse(startURL)
result.originValidator = originvalidator.NewOriginValidator(result.startURL, appoptions.BindingsAllowedOrigins)
if _starturl, _ := ctx.Value("starturl").(*url.URL); _starturl != nil {
result.startURL = _starturl
result.originValidator = originvalidator.NewOriginValidator(result.startURL, appoptions.BindingsAllowedOrigins)
return result
}
if port, _ := ctx.Value("assetserverport").(string); port != "" {
result.startURL.Host = net.JoinHostPort(result.startURL.Host, port)
result.originValidator = originvalidator.NewOriginValidator(result.startURL, appoptions.BindingsAllowedOrigins)
}
var bindings string
@ -680,7 +686,24 @@ var edgeMap = map[string]uintptr{
"nw-resize": w32.HTTOPLEFT,
}
func (f *Frontend) processMessage(message string) {
func (f *Frontend) processMessage(message string, sender *edge.ICoreWebView2, args *edge.ICoreWebView2WebMessageReceivedEventArgs) {
topSource, err := sender.GetSource()
if err != nil {
f.logger.Error(fmt.Sprintf("Unable to get source from sender: %s", err.Error()))
return
}
senderSource, err := args.GetSource()
if err != nil {
f.logger.Error(fmt.Sprintf("Unable to get source from args: %s", err.Error()))
return
}
// verify both topSource and sender are allowed origins
if !f.validBindingOrigin(topSource) || !f.validBindingOrigin(senderSource) {
return
}
if message == "drag" {
if !f.mainWindow.IsFullScreen() {
err := f.startDrag()
@ -725,6 +748,23 @@ func (f *Frontend) processMessage(message string) {
}
func (f *Frontend) processMessageWithAdditionalObjects(message string, sender *edge.ICoreWebView2, args *edge.ICoreWebView2WebMessageReceivedEventArgs) {
topSource, err := sender.GetSource()
if err != nil {
f.logger.Error(fmt.Sprintf("Unable to get source from sender: %s", err.Error()))
return
}
senderSource, err := args.GetSource()
if err != nil {
f.logger.Error(fmt.Sprintf("Unable to get source from args: %s", err.Error()))
return
}
// verify both topSource and sender are allowed origins
if !f.validBindingOrigin(topSource) || !f.validBindingOrigin(senderSource) {
return
}
if strings.HasPrefix(message, "file:drop") {
if !f.frontendOptions.DragAndDrop.EnableFileDrop {
return
@ -783,6 +823,20 @@ func (f *Frontend) processMessageWithAdditionalObjects(message string, sender *e
}
}
func (f *Frontend) validBindingOrigin(source string) bool {
origin, err := f.originValidator.GetOriginFromURL(source)
if err != nil {
f.logger.Error(fmt.Sprintf("Error parsing source URL %s: %v", source, err.Error()))
return false
}
allowed := f.originValidator.IsOriginAllowed(origin)
if !allowed {
f.logger.Error("Blocked request from unauthorized origin: %s", origin)
return false
}
return true
}
func (f *Frontend) dispatchMessage(message string) {
result, err := f.dispatcher.ProcessMessage(message, f)
if err != nil {

View file

@ -0,0 +1,116 @@
package originvalidator
import (
"fmt"
"net/url"
"regexp"
"strings"
)
type OriginValidator struct {
allowedOrigins []string
}
// NewOriginValidator creates a new validator from a comma-separated string of allowed origins
func NewOriginValidator(startUrl *url.URL, allowedOriginsString string) *OriginValidator {
allowedOrigins := startUrl.Scheme + "://" + startUrl.Host
if allowedOriginsString != "" {
allowedOrigins += "," + allowedOriginsString
}
validator := &OriginValidator{}
validator.parseAllowedOrigins(allowedOrigins)
return validator
}
// parseAllowedOrigins parses the comma-separated origins string
func (v *OriginValidator) parseAllowedOrigins(originsString string) {
if originsString == "" {
v.allowedOrigins = []string{}
return
}
origins := strings.Split(originsString, ",")
var trimmedOrigins []string
for _, origin := range origins {
trimmed := strings.TrimSuffix(strings.TrimSpace(origin), "/")
if trimmed != "" {
trimmedOrigins = append(trimmedOrigins, trimmed)
}
}
v.allowedOrigins = trimmedOrigins
}
// IsOriginAllowed checks if the given origin is allowed
func (v *OriginValidator) IsOriginAllowed(origin string) bool {
if origin == "" {
return false
}
for _, allowedOrigin := range v.allowedOrigins {
if v.matchesOriginPattern(allowedOrigin, origin) {
return true
}
}
return false
}
// matchesOriginPattern checks if origin matches the pattern (supports wildcards)
func (v *OriginValidator) matchesOriginPattern(pattern, origin string) bool {
// Exact match
if pattern == origin {
return true
}
// Wildcard pattern matching
if strings.Contains(pattern, "*") {
regexPattern := v.wildcardPatternToRegex(pattern)
matched, err := regexp.MatchString(regexPattern, origin)
if err != nil {
return false
}
return matched
}
return false
}
// wildcardPatternToRegex converts wildcard pattern to regex
func (v *OriginValidator) wildcardPatternToRegex(wildcardPattern string) string {
// Escape special regex characters except *
specialChars := []string{"\\", ".", "+", "?", "^", "$", "{", "}", "(", ")", "|", "[", "]"}
escaped := wildcardPattern
for _, specialChar := range specialChars {
escaped = strings.ReplaceAll(escaped, specialChar, "\\"+specialChar)
}
// Replace * with .* (matches any characters)
escaped = strings.ReplaceAll(escaped, "*", ".*")
// Anchor the pattern to match the entire string
return "^" + escaped + "$"
}
// GetOriginFromURL extracts origin from URL string
func (v *OriginValidator) GetOriginFromURL(urlString string) (string, error) {
if urlString == "" {
return "", fmt.Errorf("empty URL")
}
parsedURL, err := url.Parse(urlString)
if err != nil {
return "", fmt.Errorf("invalid URL: %v", err)
}
if parsedURL.Scheme == "" || parsedURL.Host == "" {
return "", fmt.Errorf("URL missing scheme or host")
}
// Build origin (scheme + host)
origin := parsedURL.Scheme + "://" + parsedURL.Host
return origin, nil
}

View file

@ -101,6 +101,9 @@ type App struct {
// DisablePanicRecovery disables the panic recovery system in messages processing
DisablePanicRecovery bool
// List of additional allowed origins for bindings in format "https://*.myapp.com,https://example.com"
BindingsAllowedOrigins string
}
type ErrorFormatter func(error) any

View file

@ -134,6 +134,7 @@ func main() {
Debug: options.Debug{
OpenInspectorOnStartup: false,
},
BindingsAllowedOrigins: "https://my.topapp,https://*.wails.isgreat",
})
if err != nil {
@ -619,8 +620,8 @@ Type: `bool`
Prevents window contents from being captured by other applications.
On Windows it calls SetWindowDisplayAffinity with `WDA_EXCLUDEFROMCAPTURE`.
For Windows 10 version 2004 and later the window will be completely removed from capture.
On Windows it calls SetWindowDisplayAffinity with `WDA_EXCLUDEFROMCAPTURE`.
For Windows 10 version 2004 and later the window will be completely removed from capture.
Older Windows versions will call SetWindowDisplayAffinity with `WDA_MONITOR`, capturing a black window.
Name: ContentProtection<br/>
@ -940,7 +941,7 @@ Type: `bool`
Prevents window contents from being captured by other applications.
On MacOS it sets the NSWindow's sharingType to NSWindowSharingNone, removing the window from capture entirely.
On MacOS it sets the NSWindow's sharingType to NSWindowSharingNone, removing the window from capture entirely.
Name: ContentProtection<br/>
Type: `bool`
@ -1129,3 +1130,12 @@ Setting this to `true` will open the WebInspector on startup of the application.
Name: OpenInspectorOnStartup<br/>
Type: `bool`
### BindingsAllowedOrigins
Comma-separated list of additional allowed origins for JS ↔ Go bindings.
Supports “*” wildcards in hostnames for subdomain matching.
Example: `"https://*.myapp.com, https://example.com"`
Name: BindingsAllowedOrigins<br/>
Type: `string`<br/>

View file

@ -16,6 +16,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Added
- Add origin verification for bindings by @APshenkin in [PR](https://github.com/wailsapp/wails/pull/4480)
- Configure Vite timeout by @leaanthony in [PR](https://github.com/wailsapp/wails/pull/4374)
- Added `ContentProtection` option to allow hiding the application window from screen sharing software [#4241](https://github.com/wailsapp/wails/pull/4241) by [@Taiterbase](https://github.com/Taiterbase)
- Added `build:tags` to project specification for automatically adding compilation tags by @symball in [PR](https://github.com/wailsapp/wails/pull/4439)