Restrict allow-same-origin to plugins that actually ARE AND request it

Only grant allow-same-origin sandbox permission to plugins that
explicitly request it AND are hosted on the same origin as the
application. Cross-origin plugins requesting same-origin are denied
with a warning log.

To request same-origin, you need to add this in the local plugin
configurarzin, for example as follows:

```
{
  "name" : "Export",
  "desc" : "Exports Kalamar results",
  "embed" : [{
    "panel" : "result",
    "title" : "exports KWICs and snippets",
    "icon" : "\uf019",
    "classes" : ["button-icon", "plugin" ],
    "onClick" : {
      "action" : "addWidget",
      "template" : "https://korap.ids-mannheim.de/instance/test-docker/plugin/export/export",
      "permissions" : ["forms", "scripts", "downloads", "same-origin" ]
    }
  }]
}
```

Change-Id: Ifcaddc4f39023c4d885921b2d527f5748811c78d
diff --git a/Changes b/Changes
index fcf47d5..fe45132 100644
--- a/Changes
+++ b/Changes
@@ -11,6 +11,7 @@
         - Fix test suite for "allow-same-origin" sandbox rule (diewald)
         - Escape commas and asterisks in query-by-match query creator (kupietz, hebasta)
         - Tests for configurable hint foundries added (hebasta)
+        - Restrict allow-same-origin to plugins that actually ARE AND request it (Requires the export plugin >= 0.4.1)(kupietz, tests hebasta)
 
 0.64 2026-02-14
         - Improve 'Plugins' mounting (diewald)
diff --git a/dev/js/spec/pluginSpec.js b/dev/js/spec/pluginSpec.js
index 4195fab..32a6e5e 100644
--- a/dev/js/spec/pluginSpec.js
+++ b/dev/js/spec/pluginSpec.js
@@ -204,7 +204,8 @@
           title : 'Add',
           onClick : {
             template : 'about:blank',
-            action : 'setWidget'
+            action : 'setWidget',
+            permissions: ['same-origin'] // Temporary
           }
         }]
       });
@@ -417,10 +418,10 @@
       expect(b.getAttribute("title")).toEqual("Add something");
       b.click();
       expect(p.element().querySelectorAll("iframe").length).toEqual(1);
-      expect(p.element().querySelector("iframe").getAttribute('sandbox')).toEqual('allow-forms allow-scripts allow-same-origin'); // Temporary
+      expect(p.element().querySelector("iframe").getAttribute('sandbox')).toEqual('allow-forms allow-scripts');
     });
   });
-  
+
   describe('KorAP.Plugin.Widget', function () {
     it('should be initializable', function () {
       expect(function () { widgetClass.create() }).toThrow(new Error("Service not well defined"));
@@ -447,7 +448,7 @@
 
       var iframe = we.firstChild;
       expect(iframe.tagName).toEqual("IFRAME");
-      expect(iframe.getAttribute("sandbox")).toEqual("allow-forms allow-scripts allow-same-origin");  // Temporary
+      expect(iframe.getAttribute("sandbox")).toEqual("allow-forms allow-scripts");
       expect(iframe.getAttribute("src")).toEqual("https://example");
       expect(iframe.getAttribute("name")).toEqual("56");
       
@@ -507,6 +508,31 @@
       expect(i.getAttribute("name")).toEqual(''+service.id);
       expect(i.getAttribute("src")).toEqual(service.src);
     });
+    
+    // Temporary
+    it('should grant same-origin for same-origin plugins', function () {
+    // about:blank inherits current origin
+    let service = serviceClass.create({
+      "name": "Test",
+      "src": window.location.origin + "/plugin.html",
+      "id": 1,
+      "permissions": ["same-origin"]
+    });
+    let iframe = service.load();
+    expect(iframe.getAttribute("sandbox")).toContain("allow-same-origin");
+    });
+    //Temporary
+    it('should deny same-origin for cross-origin plugins', function () {
+    let service = serviceClass.create({
+      "name": "Test", 
+      "src": "https://evil.example.com/plugin.html",
+      "id": 2,
+      "permissions": ["same-origin"]
+    });
+    let iframe = service.load();
+    expect(iframe.getAttribute("sandbox")).not.toContain("allow-same-origin");
+   });
+
   });
   
   describe('KorAP.Plugin.QueryPanel', function () {
diff --git a/dev/js/src/plugin/service.js b/dev/js/src/plugin/service.js
index aa00182..ef6836c 100644
--- a/dev/js/src/plugin/service.js
+++ b/dev/js/src/plugin/service.js
@@ -3,14 +3,28 @@
 define(function () {
 
   // Limit the supported sandbox permissions, especially
-  // to disallow 'same-origin'.
+  // to disallow 'same-origin' unless explicitly requested
+  // and the plugin is hosted on the same origin.
   let allowed = {
     "scripts" : 1,
     "presentation" : 1,
     "forms": 1,
     "downloads-without-user-activation" : 1,
     "downloads" : 1,
-    "popups" : 1
+    "popups" : 1,
+    "same-origin" : 1
+  };
+
+  /**
+   * Check if a URL is on the same origin as the current page.
+   */
+  function _isSameOrigin (src) {
+    try {
+      const url = new URL(src, window.location.href);
+      return url.origin === window.location.origin;
+    } catch (e) {
+      return false;
+    }
   };
 
   return {
@@ -71,7 +85,14 @@
       e.setAttribute('frameborder', 0);
       // Allow forms in Plugins
       let permissions = Array.from(this._perm).sort().map(function(i){ return "allow-"+i });
-      permissions.push("allow-same-origin");
+
+      // Only grant same-origin if plugin explicitly requested it
+      // AND is hosted on the same origin (security gate)
+      if (this._perm.has("same-origin") && !_isSameOrigin(this.src)) {
+        permissions = permissions.filter(function(p) { return p !== "allow-same-origin" });
+        KorAP.log(0, "Ignoring same-origin permission for cross-origin plugin");
+      };
+
       e.setAttribute('sandbox', permissions.join(" "));
       e.style.height = '0px';
       e.setAttribute('name', this.id);