mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Alerting: Encode path separators to side-step proxies (#58141)
This commit is contained in:
parent
7bb76e0975
commit
e410dfbab8
@ -6,7 +6,7 @@ import {
|
|||||||
RulerRecordingRuleDTO,
|
RulerRecordingRuleDTO,
|
||||||
} from 'app/types/unified-alerting-dto';
|
} from 'app/types/unified-alerting-dto';
|
||||||
|
|
||||||
import { hashRulerRule } from './rule-id';
|
import { hashRulerRule, parse, stringifyIdentifier } from './rule-id';
|
||||||
|
|
||||||
describe('hashRulerRule', () => {
|
describe('hashRulerRule', () => {
|
||||||
it('should not hash unknown rule types', () => {
|
it('should not hash unknown rule types', () => {
|
||||||
@ -59,4 +59,52 @@ describe('hashRulerRule', () => {
|
|||||||
|
|
||||||
expect(hashRulerRule(grafanaRule)).toBe(RULE_UID);
|
expect(hashRulerRule(grafanaRule)).toBe(RULE_UID);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should correctly encode and decode unix-style path separators', () => {
|
||||||
|
const identifier = {
|
||||||
|
ruleSourceName: 'my-datasource',
|
||||||
|
namespace: 'folder1/folder2',
|
||||||
|
groupName: 'group1/group2',
|
||||||
|
ruleHash: 'abc123',
|
||||||
|
};
|
||||||
|
|
||||||
|
const encodedIdentifier = encodeURIComponent(stringifyIdentifier(identifier));
|
||||||
|
|
||||||
|
expect(encodedIdentifier).toBe('pri%24my-datasource%24folder1%1Ffolder2%24group1%1Fgroup2%24abc123');
|
||||||
|
expect(encodedIdentifier).not.toContain('%2F');
|
||||||
|
expect(parse(encodedIdentifier, true)).toStrictEqual(identifier);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should correctly decode regular encoded path separators (%2F)', () => {
|
||||||
|
const identifier = {
|
||||||
|
ruleSourceName: 'my-datasource',
|
||||||
|
namespace: 'folder1/folder2',
|
||||||
|
groupName: 'group1/group2',
|
||||||
|
ruleHash: 'abc123',
|
||||||
|
};
|
||||||
|
|
||||||
|
expect(parse('pri%24my-datasource%24folder1%2Ffolder2%24group1%2Fgroup2%24abc123', true)).toStrictEqual(identifier);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should correctly encode and decode windows-style path separators', () => {
|
||||||
|
const identifier = {
|
||||||
|
ruleSourceName: 'my-datasource',
|
||||||
|
namespace: 'folder1\\folder2',
|
||||||
|
groupName: 'group1\\group2',
|
||||||
|
ruleHash: 'abc123',
|
||||||
|
};
|
||||||
|
|
||||||
|
const encodedIdentifier = encodeURIComponent(stringifyIdentifier(identifier));
|
||||||
|
|
||||||
|
expect(encodedIdentifier).toBe('pri%24my-datasource%24folder1%1Efolder2%24group1%1Egroup2%24abc123');
|
||||||
|
expect(parse(encodedIdentifier, true)).toStrictEqual(identifier);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should correctly decode a Grafana managed rule id', () => {
|
||||||
|
expect(parse('abc123', false)).toStrictEqual({ uid: 'abc123', ruleSourceName: 'grafana' });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should throw for malformed identifier', () => {
|
||||||
|
expect(() => parse('foo$bar$baz', false)).toThrow(/failed to parse/i);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
@ -91,10 +91,25 @@ function escapeDollars(value: string): string {
|
|||||||
return value.replace(/\$/g, '_DOLLAR_');
|
return value.replace(/\$/g, '_DOLLAR_');
|
||||||
}
|
}
|
||||||
|
|
||||||
function unesacapeDollars(value: string): string {
|
function unescapeDollars(value: string): string {
|
||||||
return value.replace(/\_DOLLAR\_/g, '$');
|
return value.replace(/\_DOLLAR\_/g, '$');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* deal with Unix-style path separators "/" (replaced with \x1f – unit separator)
|
||||||
|
* and Windows-style path separators "\" (replaced with \x1e – record separator)
|
||||||
|
* we need this to side-step proxies that automatically decode %2F to prevent path traversal attacks
|
||||||
|
* we'll use some non-printable characters from the ASCII table that will get encoded properly but very unlikely
|
||||||
|
* to ever be used in a rule name or namespace
|
||||||
|
*/
|
||||||
|
function escapePathSeparators(value: string): string {
|
||||||
|
return value.replace(/\//g, '\x1f').replace(/\\/g, '\x1e');
|
||||||
|
}
|
||||||
|
|
||||||
|
function unescapePathSeparators(value: string): string {
|
||||||
|
return value.replace(/\x1f/g, '/').replace(/\x1e/g, '\\');
|
||||||
|
}
|
||||||
|
|
||||||
export function parse(value: string, decodeFromUri = false): RuleIdentifier {
|
export function parse(value: string, decodeFromUri = false): RuleIdentifier {
|
||||||
const source = decodeFromUri ? decodeURIComponent(value) : value;
|
const source = decodeFromUri ? decodeURIComponent(value) : value;
|
||||||
const parts = source.split('$');
|
const parts = source.split('$');
|
||||||
@ -104,7 +119,7 @@ export function parse(value: string, decodeFromUri = false): RuleIdentifier {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (parts.length === 5) {
|
if (parts.length === 5) {
|
||||||
const [prefix, ruleSourceName, namespace, groupName, hash] = parts.map(unesacapeDollars);
|
const [prefix, ruleSourceName, namespace, groupName, hash] = parts.map(unescapeDollars).map(unescapePathSeparators);
|
||||||
|
|
||||||
if (prefix === cloudRuleIdentifierPrefix) {
|
if (prefix === cloudRuleIdentifierPrefix) {
|
||||||
return { ruleSourceName, namespace, groupName, rulerRuleHash: hash };
|
return { ruleSourceName, namespace, groupName, rulerRuleHash: hash };
|
||||||
@ -145,6 +160,7 @@ export function stringifyIdentifier(identifier: RuleIdentifier): string {
|
|||||||
]
|
]
|
||||||
.map(String)
|
.map(String)
|
||||||
.map(escapeDollars)
|
.map(escapeDollars)
|
||||||
|
.map(escapePathSeparators)
|
||||||
.join('$');
|
.join('$');
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -157,6 +173,7 @@ export function stringifyIdentifier(identifier: RuleIdentifier): string {
|
|||||||
]
|
]
|
||||||
.map(String)
|
.map(String)
|
||||||
.map(escapeDollars)
|
.map(escapeDollars)
|
||||||
|
.map(escapePathSeparators)
|
||||||
.join('$');
|
.join('$');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user