diff --git a/e2e/custom-plugins/frontend-sandbox-panel-test/module.js b/e2e/custom-plugins/frontend-sandbox-panel-test/module.js
new file mode 100644
index 00000000000..40f14ab14c7
--- /dev/null
+++ b/e2e/custom-plugins/frontend-sandbox-panel-test/module.js
@@ -0,0 +1,125 @@
+/*
+ * This is a dummy plugin to test the frontend sandbox
+ * It is not meant to be used in any other way
+ * This file doesn't require any compilation
+ */
+define(['react', '@grafana/data'], function (React, grafanaData) {
+ const HelloWorld = () => {
+ const createIframe = () => {
+ // direct iframe creation
+ const iframe = document.createElement('iframe');
+ iframe.src = 'about:blank';
+ iframe.id = 'createElementIframe';
+ iframe.style.width = '10%';
+ iframe.style.height = '10%';
+ iframe.style.border = 'none';
+ document.body.appendChild(iframe);
+
+ // via innerHTML
+ const div = document.createElement('div');
+ document.body.appendChild(div);
+ div.innerHTML =
+ '';
+
+ // via append
+ const appendIframe = document.createElement('iframe');
+ appendIframe.src = 'about:blank';
+ appendIframe.id = 'appendIframe';
+ appendIframe.style.width = '10%';
+ appendIframe.style.height = '10%';
+ appendIframe.style.border = 'none';
+ document.body.append(appendIframe);
+
+ // via prepend
+ const prependIframe = document.createElement('iframe');
+ prependIframe.src = 'about:blank';
+ prependIframe.id = 'prependIframe';
+ prependIframe.style.width = '10%';
+ prependIframe.style.height = '10%';
+ prependIframe.style.border = 'none';
+ document.body.prepend(prependIframe);
+
+ // via after
+ const referenceElementAfter = document.createElement('div');
+ document.body.appendChild(referenceElementAfter);
+ const afterIframe = document.createElement('iframe');
+ afterIframe.src = 'about:blank';
+ afterIframe.id = 'afterIframe';
+ afterIframe.style.width = '10%';
+ afterIframe.style.height = '10%';
+ afterIframe.style.border = 'none';
+ referenceElementAfter.after(afterIframe);
+
+ // via before
+ const referenceElementBefore = document.createElement('div');
+ document.body.appendChild(referenceElementBefore);
+ const beforeIframe = document.createElement('iframe');
+ beforeIframe.src = 'about:blank';
+ beforeIframe.id = 'beforeIframe';
+ beforeIframe.style.width = '10%';
+ beforeIframe.style.height = '10%';
+ beforeIframe.style.border = 'none';
+ referenceElementBefore.before(beforeIframe);
+
+ // via outerHTML
+ const outerHTMLIframe = document.createElement('iframe');
+ outerHTMLIframe.src = 'about:blank';
+ outerHTMLIframe.id = 'outerHTMLIframeTemp';
+ outerHTMLIframe.style.width = '10%';
+ outerHTMLIframe.style.height = '10%';
+ outerHTMLIframe.style.border = 'none';
+ document.body.appendChild(outerHTMLIframe);
+ outerHTMLIframe.outerHTML =
+ '';
+
+ // via parseFromString
+ const iframeString =
+ '';
+ const parser = new DOMParser();
+ const parsedDoc = parser.parseFromString(iframeString, 'text/html');
+ document.body.appendChild(parsedDoc.body.firstChild);
+
+ // via insertBefore
+ const referenceForInsertBefore = document.createElement('div');
+ document.body.appendChild(referenceForInsertBefore);
+ const insertBeforeIframe = document.createElement('iframe');
+ insertBeforeIframe.src = 'about:blank';
+ insertBeforeIframe.id = 'insertBeforeIframe';
+ insertBeforeIframe.style.width = '10%';
+ insertBeforeIframe.style.height = '10%';
+ insertBeforeIframe.style.border = 'none';
+ document.body.insertBefore(insertBeforeIframe, referenceForInsertBefore);
+
+ // via replaceChild
+ const replaceChildDiv = document.createElement('div');
+ document.body.appendChild(replaceChildDiv);
+ const replaceChildIframe = document.createElement('iframe');
+ replaceChildIframe.src = 'about:blank';
+ replaceChildIframe.id = 'replaceChildIframe';
+ replaceChildIframe.style.width = '10%';
+ replaceChildIframe.style.height = '10%';
+ replaceChildIframe.style.border = 'none';
+ document.body.replaceChild(replaceChildIframe, replaceChildDiv);
+ };
+
+ const reachOut = (e) => {
+ const outsideEl = e.target.parentElement.parentElement.parentElement.parentElement.parentElement;
+ outsideEl.dataset.sandboxTest = 'true';
+ };
+
+ return React.createElement(
+ 'div',
+ { className: 'frontend-sandbox-test' },
+ React.createElement(
+ 'button',
+ { onClick: createIframe, 'data-testid': 'button-create-iframes' },
+ 'Create iframes'
+ ),
+ React.createElement('button', { onClick: reachOut, 'data-testid': 'button-reach-out' }, 'Reach out')
+ );
+ };
+
+ const plugin = new grafanaData.PanelPlugin(HelloWorld);
+
+ return { plugin };
+});
diff --git a/e2e/custom-plugins/frontend-sandbox-panel-test/plugin.json b/e2e/custom-plugins/frontend-sandbox-panel-test/plugin.json
new file mode 100644
index 00000000000..a77283a0a6b
--- /dev/null
+++ b/e2e/custom-plugins/frontend-sandbox-panel-test/plugin.json
@@ -0,0 +1,25 @@
+{
+ "$schema": "https://raw.githubusercontent.com/grafana/grafana/master/docs/sources/developers/plugins/plugin.schema.json",
+ "type": "panel",
+ "name": "Sandbox test plugin",
+ "id": "sandbox-test-panel",
+ "info": {
+ "keywords": ["panel"],
+ "description": "",
+ "author": {
+ "name": "Grafana"
+ },
+ "logos": {
+ "small": "img/logo.svg",
+ "large": "img/logo.svg"
+ },
+ "links": [],
+ "screenshots": [],
+ "version": "1.0.0",
+ "updated": "2023-06-27"
+ },
+ "dependencies": {
+ "grafanaDependency": ">=10.0",
+ "plugins": []
+ }
+}
diff --git a/e2e/dashboards/PanelSandboxDashboard.json b/e2e/dashboards/PanelSandboxDashboard.json
new file mode 100644
index 00000000000..0ea1ca81d24
--- /dev/null
+++ b/e2e/dashboards/PanelSandboxDashboard.json
@@ -0,0 +1,58 @@
+{
+ "annotations": {
+ "list": [
+ {
+ "builtIn": 1,
+ "datasource": {
+ "type": "grafana",
+ "uid": "-- Grafana --"
+ },
+ "enable": true,
+ "hide": true,
+ "iconColor": "rgba(0, 211, 255, 1)",
+ "name": "Annotations & Alerts",
+ "type": "dashboard"
+ }
+ ]
+ },
+ "editable": true,
+ "fiscalYearStartMonth": 0,
+ "graphTooltip": 0,
+ "id": 118,
+ "links": [],
+ "liveNow": false,
+ "panels": [
+ {
+ "datasource": {
+ "type": "testdata",
+ "uid": "PD8C576611E62080A"
+ },
+ "gridPos": {
+ "h": 8,
+ "w": 12,
+ "x": 0,
+ "y": 0
+ },
+ "id": 1,
+ "title": "Sandbox Panel test",
+ "type": "sandbox-test-panel"
+ }
+ ],
+ "refresh": "",
+ "schemaVersion": 38,
+ "style": "dark",
+ "tags": [],
+ "templating": {
+ "list": []
+ },
+ "time": {
+ "from": "now-6h",
+ "to": "now"
+ },
+ "timepicker": {},
+ "timezone": "",
+ "title": "Sandbox Panel Test",
+ "uid": "c46b2460-16b7-42a5-82d1-b07fbf431950",
+ "version": 1,
+ "weekStart": ""
+}
diff --git a/e2e/panels-suite/frontend-sandbox-panel.spec.ts b/e2e/panels-suite/frontend-sandbox-panel.spec.ts
new file mode 100644
index 00000000000..ccb037087e9
--- /dev/null
+++ b/e2e/panels-suite/frontend-sandbox-panel.spec.ts
@@ -0,0 +1,100 @@
+import panelSandboxDashboard from '../dashboards/PanelSandboxDashboard.json';
+import { e2e } from '../utils';
+
+const DASHBOARD_ID = 'c46b2460-16b7-42a5-82d1-b07fbf431950';
+
+describe('Panel sandbox', () => {
+ beforeEach(() => {
+ e2e.flows.login(e2e.env('USERNAME'), e2e.env('PASSWORD'), true);
+ return e2e.flows.importDashboard(panelSandboxDashboard, 1000, true);
+ });
+
+ describe('Sandbox disabled', () => {
+ beforeEach(() => {
+ e2e.flows.openDashboard({
+ uid: DASHBOARD_ID,
+ queryParams: {
+ '__feature.pluginsFrontendSandbox': false,
+ },
+ });
+ });
+
+ it('Add iframes to body', () => {
+ // this button adds iframes to the body
+ cy.get('[data-testid="button-create-iframes"]').click();
+
+ const iframeIds = [
+ 'createElementIframe',
+ 'innerHTMLIframe',
+ 'appendIframe',
+ 'prependIframe',
+ 'afterIframe',
+ 'beforeIframe',
+ 'outerHTMLIframe',
+ 'parseFromStringIframe',
+ 'insertBeforeIframe',
+ 'replaceChildIframe',
+ ];
+ iframeIds.forEach((id) => {
+ cy.get(`#${id}`).should('exist');
+ });
+ });
+
+ it('Reaches out of panel div', () => {
+ // this button reaches out of the panel div and modifies the element dataset
+ cy.get('[data-testid="button-reach-out"]').click();
+
+ cy.get('[data-sandbox-test="true"]').should('exist');
+ });
+ });
+
+ describe('Sandbox enabled', () => {
+ beforeEach(() => {
+ e2e.flows.openDashboard({
+ uid: DASHBOARD_ID,
+ queryParams: {
+ '__feature.pluginsFrontendSandbox': true,
+ },
+ });
+ cy.window().then((win) => {
+ win.localStorage.setItem('grafana.featureToggles', 'pluginsFrontendSandbox=1');
+ });
+ });
+
+ it('Does not add iframes to body', () => {
+ // this button adds 3 iframes to the body
+ cy.get('[data-testid="button-create-iframes"]').click();
+
+ const iframeIds = [
+ 'createElementIframe',
+ 'innerHTMLIframe',
+ 'appendIframe',
+ 'prependIframe',
+ 'afterIframe',
+ 'beforeIframe',
+ 'outerHTMLIframe',
+ 'parseFromStringIframe',
+ 'insertBeforeIframe',
+ 'replaceChildIframe',
+ ];
+ iframeIds.forEach((id) => {
+ cy.get(`#${id}`).should('not.exist');
+ });
+ });
+
+ it('Does not reaches out of panel div', () => {
+ // this button reaches out of the panel div and modifies the element dataset
+ cy.get('[data-testid="button-reach-out"]').click();
+
+ cy.get('[data-sandbox-test="true"]').should('not.exist');
+ });
+ });
+
+ afterEach(() => {
+ e2e.flows.revertAllChanges();
+ });
+
+ after(() => {
+ return cy.clearCookies();
+ });
+});
diff --git a/public/app/features/plugins/sandbox/sandbox_plugin_loader.ts b/public/app/features/plugins/sandbox/sandbox_plugin_loader.ts
index 6b34a48c2c5..b9ea8fc584e 100644
--- a/public/app/features/plugins/sandbox/sandbox_plugin_loader.ts
+++ b/public/app/features/plugins/sandbox/sandbox_plugin_loader.ts
@@ -72,6 +72,11 @@ async function doImportPluginModuleInSandbox(meta: PluginMeta): Promise string,
+ createScript: (string: string) => string,
+ createScriptURL: (string: string) => string,
+ },
liveTargetCallback: isLiveTarget,
// endowments are custom variables we make available to plugins in their window object
endowments: Object.getOwnPropertyDescriptors({
diff --git a/scripts/grafana-server/start-server b/scripts/grafana-server/start-server
index 577210fa03d..2c804314dad 100755
--- a/scripts/grafana-server/start-server
+++ b/scripts/grafana-server/start-server
@@ -34,6 +34,17 @@ mkdir $PROV_DIR/dashboards
cp ./scripts/grafana-server/custom.ini $RUNDIR/conf/custom.ini
cp ./conf/defaults.ini $RUNDIR/conf/defaults.ini
+echo -e "Copying custom plugins from e2e tests"
+
+mkdir -p "$RUNDIR/data/plugins"
+# when running in a local computer
+if [ -d "./e2e/custom-plugins" ]; then
+ cp -r "./e2e/custom-plugins" "$RUNDIR/data/plugins"
+# when running in CI
+elif [ -d "../e2e/custom-plugins" ]; then
+ cp -r "../e2e/custom-plugins" "$RUNDIR/data/plugins"
+fi
+
echo -e "Copy provisioning setup from devenv"
cp devenv/datasources.yaml $PROV_DIR/datasources
@@ -53,4 +64,3 @@ $RUNDIR/bin/"$ARCH"grafana-server \
# 2>&1 > $RUNDIR/output.log &
# cfg:log.level=debug \
-