Add global isSafeURL utility

- Added isSafeURL utility based on prior work in gitlab-ee
- Also added isAbsoluteOrRootRelative() and getBaseURL() utils,
  needed by isSafeURL
- Removed URL() fallback because URL() is now polyfilled
- Updated specs
parent 8ab0db4e
...@@ -121,4 +121,40 @@ export function webIDEUrl(route = undefined) { ...@@ -121,4 +121,40 @@ export function webIDEUrl(route = undefined) {
return returnUrl; return returnUrl;
} }
/**
* Returns current base URL
*/
export function getBaseURL() {
const { protocol, host } = window.location;
return `${protocol}//${host}`;
}
/**
* Returns true if url is an absolute or root-relative URL
*
* @param {String} url
*/
export function isAbsoluteOrRootRelative(url) {
return /^(https?:)?\//.test(url);
}
/**
* Checks if the provided URL is a safe URL (absolute http(s) or root-relative URL)
*
* @param {String} url that will be checked
* @returns {Boolean}
*/
export function isSafeURL(url) {
if (!isAbsoluteOrRootRelative(url)) {
return false;
}
try {
const parsedUrl = new URL(url, getBaseURL());
return ['http:', 'https:'].includes(parsedUrl.protocol);
} catch {
return false;
}
}
export { join as joinPaths } from 'path'; export { join as joinPaths } from 'path';
...@@ -107,4 +107,88 @@ describe('URL utility', () => { ...@@ -107,4 +107,88 @@ describe('URL utility', () => {
expect(url).toBe('/home/feature#install'); expect(url).toBe('/home/feature#install');
}); });
}); });
describe('getBaseURL', () => {
beforeEach(() => {
global.window = Object.create(window);
Object.defineProperty(window, 'location', {
value: {
host: 'gitlab.com',
protocol: 'https:',
},
});
});
it('returns correct base URL', () => {
expect(urlUtils.getBaseURL()).toBe('https://gitlab.com');
});
});
describe('isAbsoluteOrRootRelative', () => {
const validUrls = ['https://gitlab.com/', 'http://gitlab.com/', '/users/sign_in'];
const invalidUrls = [' https://gitlab.com/', './file/path', 'notanurl', '<a></a>'];
it.each(validUrls)(`returns true for %s`, url => {
expect(urlUtils.isAbsoluteOrRootRelative(url)).toBe(true);
});
it.each(invalidUrls)(`returns false for %s`, url => {
expect(urlUtils.isAbsoluteOrRootRelative(url)).toBe(false);
});
});
describe('isSafeUrl', () => {
const absoluteUrls = [
'http://example.org',
'http://example.org:8080',
'https://example.org',
'https://example.org:8080',
'https://192.168.1.1',
];
const rootRelativeUrls = ['/relative/link'];
const relativeUrls = ['./relative/link', '../relative/link'];
const urlsWithoutHost = ['http://', 'https://', 'https:https:https:'];
/* eslint-disable no-script-url */
const nonHttpUrls = [
'javascript:',
'javascript:alert("XSS")',
'jav\tascript:alert("XSS");',
' &#14; javascript:alert("XSS");',
'ftp://192.168.1.1',
'file:///',
'file:///etc/hosts',
];
/* eslint-enable no-script-url */
// javascript:alert('XSS')
const encodedJavaScriptUrls = [
'&#0000106&#0000097&#0000118&#0000097&#0000115&#0000099&#0000114&#0000105&#0000112&#0000116&#0000058&#0000097&#0000108&#0000101&#0000114&#0000116&#0000040&#0000039&#0000088&#0000083&#0000083&#0000039&#0000041',
'&#106;&#97;&#118;&#97;&#115;&#99;&#114;&#105;&#112;&#116;&#58;&#97;&#108;&#101;&#114;&#116;&#40;&#39;&#88;&#83;&#83;&#39;&#41;',
'&#x6A&#x61&#x76&#x61&#x73&#x63&#x72&#x69&#x70&#x74&#x3A&#x61&#x6C&#x65&#x72&#x74&#x28&#x27&#x58&#x53&#x53&#x27&#x29',
'\\u006A\\u0061\\u0076\\u0061\\u0073\\u0063\\u0072\\u0069\\u0070\\u0074\\u003A\\u0061\\u006C\\u0065\\u0072\\u0074\\u0028\\u0027\\u0058\\u0053\\u0053\\u0027\\u0029',
];
const safeUrls = [...absoluteUrls, ...rootRelativeUrls];
const unsafeUrls = [
...relativeUrls,
...urlsWithoutHost,
...nonHttpUrls,
...encodedJavaScriptUrls,
];
describe('with URL constructor support', () => {
it.each(safeUrls)('returns true for %s', url => {
expect(urlUtils.isSafeURL(url)).toBe(true);
});
it.each(unsafeUrls)('returns false for %s', url => {
expect(urlUtils.isSafeURL(url)).toBe(false);
});
});
});
}); });
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment