Authorization / Access control

HIGH
grafana/grafana
Commit: 395ed163dff9
Affected: Grafana 12.4.0 and earlier (pre-fix for splash CTA gating).
2026-04-06 13:53 UTC

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" },
← Back to Alerts View on GitHub →