diff --git a/v2/go.mod b/v2/go.mod index 3268ba778..1a40badd2 100644 --- a/v2/go.mod +++ b/v2/go.mod @@ -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 diff --git a/v2/go.sum b/v2/go.sum index 49f37b35d..53e56707e 100644 --- a/v2/go.sum +++ b/v2/go.sum @@ -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= diff --git a/v2/internal/frontend/desktop/darwin/WailsContext.m b/v2/internal/frontend/desktop/darwin/WailsContext.m index 67f9c2874..1a3f5ef7d 100644 --- a/v2/internal/frontend/desktop/darwin/WailsContext.m +++ b/v2/internal/frontend/desktop/darwin/WailsContext.m @@ -477,6 +477,15 @@ typedef void (^schemeTaskCaller)(id); } - (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); } 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 { diff --git a/v2/internal/frontend/desktop/darwin/frontend.go b/v2/internal/frontend/desktop/darwin/frontend.go index ba00b02d9..c9e70d4be 100644 --- a/v2/internal/frontend/desktop/darwin/frontend.go +++ b/v2/internal/frontend/desktop/darwin/frontend.go @@ -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 diff --git a/v2/internal/frontend/desktop/darwin/message.h b/v2/internal/frontend/desktop/darwin/message.h index 66110841d..86506f868 100644 --- a/v2/internal/frontend/desktop/darwin/message.h +++ b/v2/internal/frontend/desktop/darwin/message.h @@ -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*); diff --git a/v2/internal/frontend/desktop/linux/frontend.go b/v2/internal/frontend/desktop/linux/frontend.go index 3bc81649f..c009a18ca 100644 --- a/v2/internal/frontend/desktop/linux/frontend.go +++ b/v2/internal/frontend/desktop/linux/frontend.go @@ -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() { diff --git a/v2/internal/frontend/desktop/linux/window.c b/v2/internal/frontend/desktop/linux/window.c index ca61bdf28..5441db022 100644 --- a/v2/internal/frontend/desktop/linux/window.c +++ b/v2/internal/frontend/desktop/linux/window.c @@ -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); -} \ No newline at end of file +} diff --git a/v2/internal/frontend/desktop/windows/frontend.go b/v2/internal/frontend/desktop/windows/frontend.go index 65490ef23..a29eb851b 100644 --- a/v2/internal/frontend/desktop/windows/frontend.go +++ b/v2/internal/frontend/desktop/windows/frontend.go @@ -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 { diff --git a/v2/internal/frontend/originvalidator/originValidator.go b/v2/internal/frontend/originvalidator/originValidator.go new file mode 100644 index 000000000..fd416f945 --- /dev/null +++ b/v2/internal/frontend/originvalidator/originValidator.go @@ -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 +} diff --git a/v2/pkg/options/options.go b/v2/pkg/options/options.go index 082ac48cd..0f62d5e4b 100644 --- a/v2/pkg/options/options.go +++ b/v2/pkg/options/options.go @@ -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 diff --git a/website/docs/reference/options.mdx b/website/docs/reference/options.mdx index fae89c085..8651a3205 100644 --- a/website/docs/reference/options.mdx +++ b/website/docs/reference/options.mdx @@ -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
@@ -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
Type: `bool` @@ -1129,3 +1130,12 @@ Setting this to `true` will open the WebInspector on startup of the application. Name: OpenInspectorOnStartup
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
+Type: `string`
diff --git a/website/src/pages/changelog.mdx b/website/src/pages/changelog.mdx index 46745fbde..08bbf4f7f 100644 --- a/website/src/pages/changelog.mdx +++ b/website/src/pages/changelog.mdx @@ -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)