Allow root-relative URLs in vulnerability modal

- isSafeUrl utility now considers root-relative URLs safe
- `file` field in modal data now points to blob_path if available
parent 0f2d1809
const isAbsoluteOrRootRelative = url => /^(https?:)?\//.test(url);
/** /**
* Checks if the provided URL is a safe URL (absolute http(s) URL) * Checks if the provided URL is a safe URL (absolute http(s) URL)
* *
...@@ -7,7 +9,7 @@ ...@@ -7,7 +9,7 @@
export default url => { export default url => {
let parsedUrl; let parsedUrl;
if (!(url.startsWith('https:') || url.startsWith('http:'))) { if (!isAbsoluteOrRootRelative(url)) {
return false; return false;
} }
......
...@@ -108,6 +108,10 @@ export default { ...@@ -108,6 +108,10 @@ export default {
Object.keys(data).forEach(key => { Object.keys(data).forEach(key => {
if (data[key].value && data[key].value.length) { if (data[key].value && data[key].value.length) {
result[key] = data[key]; result[key] = data[key];
if (key === 'file' && this.vulnerability.blob_path) {
result[key].isLink = true;
result[key].url = this.vulnerability.blob_path;
}
} }
}); });
......
---
title: Make root relative URLs clickable in vulnerability modal
merge_request: 9767
author:
type: fixed
...@@ -10,7 +10,9 @@ describe('isSafeUrl', () => { ...@@ -10,7 +10,9 @@ describe('isSafeUrl', () => {
'https://192.168.1.1', 'https://192.168.1.1',
]; ];
const relativeUrls = ['./relative/link', '/relative/link', '../relative/link']; const rootRelativeUrls = ['/relative/link'];
const relativeUrls = ['./relative/link', '../relative/link'];
const urlsWithoutHost = ['http://', 'https://', 'https:https:https:']; const urlsWithoutHost = ['http://', 'https://', 'https:https:https:'];
...@@ -31,17 +33,22 @@ describe('isSafeUrl', () => { ...@@ -31,17 +33,22 @@ describe('isSafeUrl', () => {
'\\u006A\\u0061\\u0076\\u0061\\u0073\\u0063\\u0072\\u0069\\u0070\\u0074\\u003A\\u0061\\u006C\\u0065\\u0072\\u0074\\u0028\\u0027\\u0058\\u0053\\u0053\\u0027\\u0029', '\\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', () => { describe('with URL constructor support', () => {
it.each(absoluteUrls)('returns true for %s', url => { it.each(safeUrls)('returns true for %s', url => {
expect(isSafeURL(url)).toBe(true); expect(isSafeURL(url)).toBe(true);
}); });
it.each([...relativeUrls, ...urlsWithoutHost, ...nonHttpUrls, ...encodedJavaScriptUrls])( it.each(unsafeUrls)('returns false for %s', url => {
'returns false for %s', expect(isSafeURL(url)).toBe(false);
url => { });
expect(isSafeURL(url)).toBe(false);
},
);
}); });
describe('without URL constructor support', () => { describe('without URL constructor support', () => {
...@@ -51,15 +58,12 @@ describe('isSafeUrl', () => { ...@@ -51,15 +58,12 @@ describe('isSafeUrl', () => {
}); });
}); });
it.each(absoluteUrls)('returns true for %s', url => { it.each(safeUrls)('returns true for %s', url => {
expect(isSafeURL(url)).toBe(true); expect(isSafeURL(url)).toBe(true);
}); });
it.each([...relativeUrls, ...urlsWithoutHost, ...nonHttpUrls, ...encodedJavaScriptUrls])( it.each(unsafeUrls)('returns false for %s', url => {
'returns false for %s', expect(isSafeURL(url)).toBe(false);
url => { });
expect(isSafeURL(url)).toBe(false);
},
);
}); });
}); });
...@@ -97,18 +97,42 @@ describe('Security Reports modal', () => { ...@@ -97,18 +97,42 @@ describe('Security Reports modal', () => {
}); });
describe('Vulnerability Details', () => { describe('Vulnerability Details', () => {
it('is rendered', () => { const blobPath = '/group/project/blob/1ab2c3d4e5/some/file.path#L0-0';
const namespaceValue = 'foobar';
const fileValue = '/some/file.path';
beforeEach(() => {
const props = { const props = {
modal: createState().modal, modal: createState().modal,
}; };
props.modal.data.namespace.value = 'foobar'; props.modal.vulnerability.blob_path = blobPath;
props.modal.data.namespace.value = namespaceValue;
props.modal.data.file.value = fileValue;
vm = mountComponent(Component, props); vm = mountComponent(Component, props);
});
it('is rendered', () => {
const vulnerabilityDetails = vm.$el.querySelector('.js-vulnerability-details'); const vulnerabilityDetails = vm.$el.querySelector('.js-vulnerability-details');
expect(vulnerabilityDetails).not.toBeNull(); expect(vulnerabilityDetails).not.toBeNull();
expect(vulnerabilityDetails.textContent).toContain('foobar'); expect(vulnerabilityDetails.textContent).toContain('foobar');
}); });
it('computes valued fields properly', () => {
expect(vm.valuedFields).toMatchObject({
file: {
value: fileValue,
url: blobPath,
isLink: true,
text: 'File',
},
namespace: {
value: namespaceValue,
text: 'Namespace',
isLink: false,
},
});
});
}); });
describe('Solution Card', () => { describe('Solution Card', () => {
......
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