Authorization / Access control
Description
The commit hardens the splash screen CTAs by gating role-based/permission-based access to internal/admin URLs and using a safe fallback URL when the user lacks required permissions or admin role. Previously, CTAs could reveal internal admin URLs (e.g., /admin/provisioning) in the splash modal, which could be clicked by non-privileged users, potentially exposing admin endpoints or leaking UI structure. The fix computes the CTA URL based on user roles/permissions and falls back to safe external/docs URLs when the user is not authorized. This is an authorization/access-control hardening rather than a feature addition.
Proof of Concept
Proof of concept (pre-fix behavior):
1) Prereqs: Grafana 12.4.x with splash CTAs configured to point to an internal admin page (e.g., /admin/provisioning) via the git-sync CTA.
2) Log in as a non-admin user (no Admin role, no specific dashboards:create permission).
3) Open the splash modal; observe that the CTA for the Git Sync feature is a link to /admin/provisioning (internal admin page).
4) Click the CTA; backend denies access (HTTP 403 or redirect to login) or shows an admin page regardless of missing authority depending on server config.
5) Attack vector: An attacker can discover the internal admin URL from the UI and attempt direct navigation, or a crafted link could point to admin URLs.
Fix impact PoC (post-fix):
6) With the fix, the CTA URL is resolved via resolveCtaUrl. If the user lacks requiresAdmin or permission, the CTA resolves to cta.fallbackUrl or a non-admin URL (e.g., docs) instead of an internal admin path. For the git-sync CTA, the CTA would navigate to the docs page with UTMs instead of /admin/provisioning.
7) Verify by repeating steps 1-4 and ensuring the CTA now opens the fallback/docs URL, not the protected endpoint.
Code-level example (conceptual):
pre-fix: anchor href is "/admin/provisioning" in the splash CTA
post-fix: href is resolved to a safe URL based on user roles/permissions, e.g. "https://grafana.com/docs/grafana/latest/as-code/observability-as-code/git-sync?src=grafana-oss"
Commit Details
Author: Ihor Yeromin
Date: 2026-04-06 11:15 UTC
Message:
Splash Screen: Add role-based CTA links with UTM tracking (#121928)
* feat(splash-screen): add role-based CTA links with UTM tracking
* revert version
Triage Assessment
Vulnerability Type: Authorization / Access control
Confidence: HIGH
Reasoning:
The change introduces role-based and permission-based gating for CTA links in the splash screen. It resolves URLs based on user roles/permissions and uses fallback URLs, preventing unauthorized access to certain internal admin/privileged pages. This is an authorization/access control hardening rather than a pure feature tweak.
Verification Assessment
Vulnerability Type: Authorization / Access control
Confidence: HIGH
Affected Versions: Grafana 12.4.0 and earlier (pre-fix for splash CTA gating).
Code Diff
diff --git a/public/app/core/components/SplashScreenModal/SplashScreenModal.tsx b/public/app/core/components/SplashScreenModal/SplashScreenModal.tsx
index b61582f691b3..0d18decc8138 100644
--- a/public/app/core/components/SplashScreenModal/SplashScreenModal.tsx
+++ b/public/app/core/components/SplashScreenModal/SplashScreenModal.tsx
@@ -5,12 +5,27 @@ import { type GrafanaTheme2 } from '@grafana/data';
import { t } from '@grafana/i18n';
import { IconButton, LinkButton, useStyles2 } from '@grafana/ui';
import { ModalBase } from '@grafana/ui/internal';
+import { contextSrv } from 'app/core/services/context_srv';
import { SplashScreenNav } from './SplashScreenNav';
import { SplashScreenSlide } from './SplashScreenSlide';
-import { getSplashScreenConfig } from './splashContent';
+import { type SplashFeatureCta, getSplashScreenConfig } from './splashContent';
import { useShouldShowSplash } from './useShouldShowSplash';
+function resolveCtaUrl(cta: SplashFeatureCta): string {
+ if (cta.requiresAdmin && !contextSrv.hasRole('Admin')) {
+ return cta.fallbackUrl ?? cta.url;
+ }
+ if (cta.permission && !contextSrv.hasPermission(cta.permission)) {
+ return cta.fallbackUrl ?? cta.url;
+ }
+ return cta.url;
+}
+
+function isExternalUrl(url: string): boolean {
+ return url.startsWith('http');
+}
+
export function SplashScreenModal() {
const [activeIndex, setActiveIndex] = useState(0);
const styles = useStyles2(getStyles);
@@ -37,6 +52,9 @@ export function SplashScreenModal() {
}
const activeFeature = config.features[activeIndex];
+ const cta = activeFeature.cta;
+ const ctaUrl = cta ? resolveCtaUrl(cta) : '';
+ const isCtaExternal = isExternalUrl(ctaUrl);
const footer = (
<>
@@ -47,17 +65,18 @@ export function SplashScreenModal() {
onNext={goToNext}
onGoTo={setActiveIndex}
/>
- {activeFeature.ctaUrl && (
+ {cta && (
<LinkButton
- href={activeFeature.ctaUrl}
- target="_blank"
- rel="noopener noreferrer"
- icon="external-link-alt"
+ href={ctaUrl}
+ onClick={isCtaExternal ? undefined : dismiss}
+ target={isCtaExternal ? '_blank' : undefined}
+ rel={isCtaExternal ? 'noopener noreferrer' : undefined}
+ icon={isCtaExternal ? 'external-link-alt' : 'arrow-right'}
variant="secondary"
fill="outline"
size="md"
>
- {activeFeature.ctaText}
+ {cta.text}
</LinkButton>
)}
</>
diff --git a/public/app/core/components/SplashScreenModal/SplashScreenSlide.test.tsx b/public/app/core/components/SplashScreenModal/SplashScreenSlide.test.tsx
index bc89e9833914..9923d810531b 100644
--- a/public/app/core/components/SplashScreenModal/SplashScreenSlide.test.tsx
+++ b/public/app/core/components/SplashScreenModal/SplashScreenSlide.test.tsx
@@ -11,8 +11,6 @@ const mockFeature: SplashFeature = {
title: 'Test Feature Title',
subtitle: 'Test subtitle text',
bullets: ['Bullet one', 'Bullet two', 'Bullet three'],
- ctaText: 'Try it',
- ctaUrl: '/test',
heroImageUrl: 'https://placehold.co/400x400',
};
diff --git a/public/app/core/components/SplashScreenModal/splashContent.ts b/public/app/core/components/SplashScreenModal/splashContent.ts
index ed6a1dddba8f..3d583f9b1da8 100644
--- a/public/app/core/components/SplashScreenModal/splashContent.ts
+++ b/public/app/core/components/SplashScreenModal/splashContent.ts
@@ -8,6 +8,14 @@ import libraryOfThingsImage from './images/library-of-things.png';
export type AccentColorKey = 'dark-purple' | 'primary' | 'success' | 'dark-orange';
+export interface SplashFeatureCta {
+ text: string;
+ url: string;
+ fallbackUrl?: string;
+ permission?: string;
+ requiresAdmin?: boolean;
+}
+
export interface SplashFeature {
id: string;
icon: IconName;
@@ -17,8 +25,7 @@ export interface SplashFeature {
title: string;
subtitle: string;
bullets: string[];
- ctaText?: string;
- ctaUrl?: string;
+ cta?: SplashFeatureCta;
heroImageUrl: string;
}
@@ -27,6 +34,8 @@ export interface SplashScreenConfig {
features: SplashFeature[];
}
+const UTM = 'src=grafana-oss&cnt=whats-new-modal';
+
export function getSplashScreenConfig(): SplashScreenConfig {
return {
version: '13.0.0',
@@ -43,8 +52,10 @@ export function getSplashScreenConfig(): SplashScreenConfig {
t('splash-screen.assistant.bullet-2', 'Create comprehensive dashboards in minutes'),
t('splash-screen.assistant.bullet-3', 'Onboard new team members in days, not weeks'),
],
- ctaText: t('splash-screen.assistant.cta', 'Show me'),
- ctaUrl: 'https://grafana.com/grafana/plugins/grafana-assistant-app/',
+ cta: {
+ text: t('splash-screen.assistant.cta', 'Show me'),
+ url: `https://grafana.com/grafana/plugins/grafana-assistant-app/?${UTM}`,
+ },
heroImageUrl: assistantHeroImage,
},
{
@@ -65,6 +76,12 @@ export function getSplashScreenConfig(): SplashScreenConfig {
),
t('splash-screen.dynamic-dashboards.bullet-3', 'Auto-arrange panels into a grid for an efficient layout'),
],
+ cta: {
+ text: t('splash-screen.dynamic-dashboards.cta', 'Show me'),
+ url: '/dashboard/new',
+ fallbackUrl: `https://grafana.com/docs/grafana/next/visualizations/dashboards/build-dashboards/create-dashboard?${UTM}`,
+ permission: 'dashboards:create',
+ },
heroImageUrl: dynamicDashboardsImage,
},
{
@@ -85,6 +102,12 @@ export function getSplashScreenConfig(): SplashScreenConfig {
'Works with many deployment scenarios like dev-prod, HA, and instances shared by multiple teams'
),
],
+ cta: {
+ text: t('splash-screen.git-sync.cta', 'Show me'),
+ url: '/admin/provisioning',
+ fallbackUrl: `https://grafana.com/docs/grafana/latest/as-code/observability-as-code/git-sync?${UTM}`,
+ requiresAdmin: true,
+ },
heroImageUrl: gitSyncImage,
},
{
@@ -105,6 +128,12 @@ export function getSplashScreenConfig(): SplashScreenConfig {
'Explore and use community dashboards tailored to your data source'
),
],
+ cta: {
+ text: t('splash-screen.library-of-things.cta', 'Show me'),
+ url: '/dashboards?templateDashboards=true&source=whats-new-modal',
+ fallbackUrl: `https://grafana.com/docs/grafana/next/visualizations/dashboards/build-dashboards/create-template-dashboards?${UTM}`,
+ permission: 'dashboards:create',
+ },
heroImageUrl: libraryOfThingsImage,
},
],
diff --git a/public/locales/en-US/grafana.json b/public/locales/en-US/grafana.json
index f37f7cd6167c..8f6f91141209 100644
--- a/public/locales/en-US/grafana.json
+++ b/public/locales/en-US/grafana.json
@@ -14786,6 +14786,7 @@
"bullet-1": "Split content into tabs for quick switching",
"bullet-2": "Show or hide panels based on template variables and other conditions",
"bullet-3": "Auto-arrange panels into a grid for an efficient layout",
+ "cta": "Show me",
"subtitle": "Make your dashboards more impactful and easier to explore",
"title": "Add tabs and panel conditions to your dashboards"
},
@@ -14793,6 +14794,7 @@
"bullet-1": "Store your dashboard configuration safely in any git repository",
"bullet-2": "keep track of the changes - and who made them",
"bullet-3": "Works with many deployment scenarios like dev-prod, HA, and instances shared by multiple teams",
+ "cta": "Show me",
"subtitle": "Bring version control, collaboration, and reliability to your dashboards",
"title": "Sync your dashboards to Git"
},
@@ -14800,6 +14802,7 @@
"bullet-1": "Start from a clean layout instead of building from scratch",
"bullet-2": "Load a template and customize it to your needs",
"bullet-3": "Explore and use community dashboards tailored to your data source",
+ "cta": "Show me",
"subtitle": "Get to a useful dashboard sooner by starting from a proven design",
"title": "Kickstart dashboards with suggestions and templates"
},