-
Notifications
You must be signed in to change notification settings - Fork 104
Expand file tree
/
Copy pathquery-url-sanitization.ts
More file actions
166 lines (150 loc) · 4.96 KB
/
query-url-sanitization.ts
File metadata and controls
166 lines (150 loc) · 4.96 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
/* eslint-disable no-useless-escape */
import { IQuery } from '../../types/query-runner';
import {
isAllAlpha,
isAlphaNumeric,
isPlaceHolderSegment,
sanitizeQueryParameter
} from './query-parameter-sanitization';
import { parseSampleUrl } from './sample-url-generation';
// Matches strings with deprecation identifier
const DEPRECATION_REGEX = /^[a-z]+_v2$/gi;
// Matches patterns like users('MeganB@M365x214355.onmicrosoft.com')
const FUNCTION_CALL_REGEX = /^[a-z]+\(.*\)$/i;
// Matches entity and entity set name patterns like microsoft.graph.group or all letters
const ENTITY_NAME_REGEX = /^(microsoft\.graph(\.[a-z]+)+|(?![a-z.])[a-z]+)$/i;
// Matches folder/file path which is part of url e.g. /root:/FolderA/FileB.txt:/
const ITEM_PATH_REGEX = /(?:\/)[\w]+:[\w\/.]+(:(?=\/)|$)/g;
// Matches patterns like root: <value>
const SANITIZED_ITEM_PATH_REGEX = /^[a-z]+:<value>$/i;
/**
* @param segment part of the url string to test
* deprecated resources may have `_v2` temporarily
* @returns boolean
*/
export function isDeprecation(segment: string): boolean {
return DEPRECATION_REGEX.test(segment);
}
/**
* Matches patterns like users('MeganB@M365x214355.onmicrosoft.com').
* Characters before bracket must be letters only
* @param segment
*/
export function isFunctionCall(segment: string): boolean {
return FUNCTION_CALL_REGEX.test(segment);
}
/**
* Sanitize Graph API Sandbox URL used when a user is not signed in
* @param url - URL to be sanitized
*/
export function sanitizeGraphAPISandboxUrl(url: string): string {
const urlObject = new URL(url);
const queryParams = urlObject.searchParams;
// This query parameter holds Graph query URL
const queryUrl = queryParams.get('url');
if (queryUrl) {
queryParams.set('url', sanitizeQueryUrl(queryUrl));
}
return urlObject.toString();
}
/**
* @param url - query URL to be sanitized e.g. https://graph.microsoft.com/v1.0/users/{user-id}
*/
export function sanitizeQueryUrl(url: string): string {
try {
return sanitizedQueryUrl(url);
} catch (e: unknown) {
return '';
}
}
function sanitizedQueryUrl(url: string): string {
url = decodeURIComponent(url);
const { origin } = new URL(url);
const { search, queryVersion, requestUrl } = parseSampleUrl(url);
const queryString: string = search
? `?${sanitizeQueryParameters(search)}`
: '';
// Sanitize item path specified in query url
let resourceUrl = requestUrl;
if (resourceUrl) {
resourceUrl = requestUrl.replace(
ITEM_PATH_REGEX,
(match: string): string => {
return `${match.substring(0, match.indexOf(':'))}:<value>`;
}
);
// Split requestUrl into segments that can be sanitized individually
const urlSegments = resourceUrl.split('/');
urlSegments.forEach((segment, index) => {
const sanitizedSegment = sanitizePathSegment(
urlSegments[index - 1],
segment
);
resourceUrl = resourceUrl.replace(segment, sanitizedSegment);
});
}
return `${origin}/${queryVersion}/${resourceUrl}${queryString}`;
}
/**
* Skipped segments:
* - Entities, entity sets and navigation properties, expected to contain alphabetic letters only
* - Deprecated entities in the form <entity>_v2
* The remaining URL segments are assumed to be variables that need to be sanitized
* @param segment
*/
function sanitizePathSegment(previousSegment: string, segment: string): string {
if (
isAllAlpha(segment) ||
isAlphaNumeric(segment) ||
isDeprecation(segment) ||
SANITIZED_ITEM_PATH_REGEX.test(segment) ||
segment.startsWith('$') ||
ENTITY_NAME_REGEX.test(segment)
) {
return segment;
}
// Check if segment is in this form: users('<some-id>|<UPN>') and tranform to users(<value>)
if (isFunctionCall(segment)) {
const openingBracketIndex = segment.indexOf('(');
const textWithinBrackets = segment.substr(
openingBracketIndex + 1,
segment.length - 2
);
const sanitizedText = textWithinBrackets
.split(',')
.map((text) => {
if (text.includes('=')) {
let key = text.split('=')[0];
key = !isAllAlpha(key) ? '<key>' : key;
return `${key}=<value>`;
}
return '<value>';
})
.join(',');
return `${segment.substring(0, openingBracketIndex)}(${sanitizedText})`;
}
if (isPlaceHolderSegment(segment)) {
return segment;
}
if (!isAllAlpha(previousSegment) && !isDeprecation(previousSegment)) {
previousSegment = 'unknown';
}
return `{${previousSegment}-id}`;
}
/**
* Remove variable data from each query parameter
* @param queryString
*/
function sanitizeQueryParameters(queryString: string): string {
// remove leading ? from query string and decode
queryString = decodeURIComponent(
queryString.substring(1).replace(/\+/g, ' ')
);
return queryString.split('&').map(sanitizeQueryParameter).join('&');
}
export function encodeHashCharacters(query: IQuery): string {
if (query.sampleUrl) {
return query.sampleUrl.replace(/#/g, '%2523');
}
return '';
}