CVE-2022-39324 (Other, published 2023-01-27T23:15Z)
Description: value: Grafana is an open-source platform for monitoring and observability. Prior to versions 8.5.16 and 9.2.8, malicious user can create a snapshot and arbitrarily choose the `originalUrl` parameter by editing the query, thanks to a web proxy. When another user opens the URL of the snapshot, they will be presented with the regular web interface delivered by the trusted Grafana server. The `Open original dashboard` button no longer points to the to the real original dashboard but to the attacker’s injected URL. This issue is fixed in versions 8.5.16 and 9.2.8.
Severity: nan (CVSS v3 base score: 3.5)
Weakness: CWE-79 \u2014 Improper Neutralization of Input During Web Page Generation ('Cross-site Scripting')
The product does not neutralize or incorrectly neutralizes user-controllable input before it is placed in output that is used as a web page that is served to other users.
Repository: https://github.com/grafana/grafana
Files changed:
packages/grafana-ui/src/components/ConfirmModal/ConfirmModal.tsx
pkg/api/dashboard_snapshot.go
public/app/features/dashboard/components/DashNav/DashNav.tsx
public/app/features/dashboard/components/ShareModal/ShareSnapshot.tsx
Commit message: Snapshots: Build snapshot originalUrl on the backend  (#60232)

* Snapshots: Fix originalUrl spoof security issue

* Store relative URL only & validate UID

* use existing modal management tools

* Dummy commit to nudge CI

* Remove unused hooks file

* Fix import after backport

* Backport fixes

Co-authored-by: kay delaney <kay@grafana.com>

Vulnerable code:
        <Button variant="secondary" onClick={onDismiss} fill="outline">
	var url string
		url = response.Url
		url = setting.ToAbsUrl("dashboard/snapshot/" + cmd.Key)
		"url":       url,
import { DashboardMetaChangedEvent } from 'app/types/events';
          onClick={() => gotoSnapshotOrigin(snapshotUrl)}
  const gotoSnapshotOrigin = (snapshotUrl: string) => {
    window.location.href = textUtil.sanitizeUrl(snapshotUrl);
  };

    if (!external) {
      this.dashboard.snapshot.originalUrl = window.location.href;
    }


Fixed code:
  /** Variant for confirm button */
  confirmVariant?: ButtonVariant;
  /** Variant for dismiss button */
  dismissVariant?: ButtonVariant;
  confirmVariant = 'destructive',
  dismissVariant = 'secondary',
        <Button variant={dismissVariant} onClick={onDismiss} fill="outline">
func createOriginalDashboardURL(appURL string, cmd *dashboardsnapshots.CreateDashboardSnapshotCommand) (string, error) {
	dashUID := cmd.Dashboard.Get("uid").MustString("")
	if ok := util.IsValidShortUID(dashUID); !ok {
		return "", fmt.Errorf("invalid dashboard UID")
	}

	return fmt.Sprintf("/d/%v", dashUID), nil
}

	var snapshotUrl string
	originalDashboardURL, err := createOriginalDashboardURL(hs.Cfg.AppURL, &cmd)
	if err != nil {
		return response.Error(http.StatusInternalServerError, "Invalid app URL", err)
	}
		snapshotUrl = response.Url
		cmd.Dashboard.SetPath([]string{"snapshot", "originalUrl"}, originalDashboardURL)

		snapshotUrl = setting.ToAbsUrl("dashboard/snapshot/" + cmd.Key)
		"url":       snapshotUrl,
import { css } from '@emotion/css';
  ConfirmModal,
import { useAppNotification } from 'app/core/copy/appNotification';
import { appEvents } from 'app/core/core';
import { DashboardMetaChangedEvent, ShowModalReactEvent } from 'app/types/events';
  const originalUrl = props.dashboard.snapshot?.originalUrl ?? '';
  const gotoSnapshotOrigin = () => {
    window.location.href = textUtil.sanitizeUrl(props.dashboard.snapshot.originalUrl);
  };

  const notifyApp = useAppNotification();
  const onOpenSnapshotOriginal = () => {
    try {
      const sanitizedUrl = new URL(textUtil.sanitizeUrl(originalUrl), config.appUrl);
      const appUrl = new URL(config.appUrl);
      if (sanitizedUrl.host !== appUrl.host) {
        appEvents.publish(
          new ShowModalReactEvent({
            component: ConfirmModal,
            props: {
              title: 'Proceed to external site?',
              modalClass: modalStyles,
              body: (
                <>
                  <p>
                    {`This link connects to an external website at`} <code>{originalUrl}</code>
                  </p>
                  <p>{"Are you sure you'd like to proceed?"}</p>
                </>
              ),
              confirmVariant: 'primary',
              confirmText: 'Proceed',
              onConfirm: gotoSnapshotOrigin,
            },
          })
        );
      } else {
        gotoSnapshotOrigin();
      }
    } catch (err) {
      notifyApp.error('Invalid URL', err instanceof Error ? err.message : undefined);
    }
  };

          onClick={onOpenSnapshotOriginal}

const modalStyles = css({
  width: 'max-content',
  maxWidth: '80vw',
});