From c37bb1d0a61c4a273176f2972e82e5bfe3c51c70 Mon Sep 17 00:00:00 2001 From: Kristian Bremberg <114284895+KristianGrafana@users.noreply.github.com> Date: Tue, 30 Sep 2025 15:39:14 +0200 Subject: [PATCH] fix: don't split query URL params in validatePath (#111296) --- .../grafana-data/src/text/sanitize.test.ts | 6 +++--- packages/grafana-data/src/text/sanitize.ts | 20 +++++++++---------- 2 files changed, 12 insertions(+), 14 deletions(-) diff --git a/packages/grafana-data/src/text/sanitize.test.ts b/packages/grafana-data/src/text/sanitize.test.ts index a0c69838976..f6a43d4a943 100644 --- a/packages/grafana-data/src/text/sanitize.test.ts +++ b/packages/grafana-data/src/text/sanitize.test.ts @@ -167,9 +167,9 @@ describe('validatePath', () => { expect(validatePath(urlWithDots)).toBe(urlWithDots); }); - it('should allow query parameters that contain dots', () => { - const urlWithDotsInQuery = 'https://api.example.com/search?version=1.2.3&file=../config'; - expect(validatePath(urlWithDotsInQuery)).toBe(urlWithDotsInQuery); + it('should block query parameters that contain path traversal', () => { + const urlWithTraversalInQuery = 'https://api.example.com/search?version=1.2.3&file=../config'; + expect(() => validatePath(urlWithTraversalInQuery)).toThrow(PathValidationError); }); it('should handle malformed URLs gracefully', () => { diff --git a/packages/grafana-data/src/text/sanitize.ts b/packages/grafana-data/src/text/sanitize.ts index 2dae143ff69..121466ab81a 100644 --- a/packages/grafana-data/src/text/sanitize.ts +++ b/packages/grafana-data/src/text/sanitize.ts @@ -146,27 +146,25 @@ export class PathValidationError extends Error { */ export function validatePath(path: OriginalPath): OriginalPath { try { - let originalDecoded: string = path; // down-cast to a string to indicate this can't be returned + let decoded: string = path; while (true) { - const nextDecode = decodeURIComponent(originalDecoded); - if (nextDecode === originalDecoded) { + const nextDecode = decodeURIComponent(decoded); + if (nextDecode === decoded) { break; // String is fully decoded. } - originalDecoded = nextDecode; + decoded = nextDecode; } - // Remove query params and fragments to check only the path portion - const cleaned = originalDecoded.split(/[\?#]/)[0]; - originalDecoded = cleaned; - - // If the original string contains traversal attempts, block it - if (/\.\.|\/\\|[\t\n\r]/.test(originalDecoded)) { + // Validate the entire decoded string for traversal attempts + // This prevents attacks that use query separators to hide traversal payloads + if (/\.\.|\/\\|[\t\n\r]/.test(decoded)) { throw new PathValidationError(); } + // Return the original path (not the decoded version) to preserve the full URL return path; } catch (err) { - // Rethrow the original InvalidPathError to preserve the stack trace + // Rethrow the original PathValidationError to preserve the stack trace if (err instanceof PathValidationError) { throw err; }