Browse Source

SONAR-11867, SSF-74 Fix XSS in project links on account/projects

tags/7.8
Wouter Admiraal 5 years ago
parent
commit
2beaf73c2d

+ 19
- 15
server/sonar-web/src/main/js/apps/account/projects/ProjectCard.tsx View File

@@ -18,14 +18,14 @@
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
import * as React from 'react';
import { sortBy } from 'lodash';
import { Link } from 'react-router';
import DateFromNow from '../../../components/intl/DateFromNow';
import DateTimeFormatter from '../../../components/intl/DateTimeFormatter';
import HelpTooltip from '../../../components/controls/HelpTooltip';
import Level from '../../../components/ui/Level';
import ProjectLinkIcon from '../../../components/icons-components/ProjectLinkIcon';
import Tooltip from '../../../components/controls/Tooltip';
import MetaLink from '../../overview/meta/MetaLink';
import { orderLinks } from '../../projectLinks/utils';
import { translateWithParameters, translate } from '../../../helpers/l10n';

interface Props {
@@ -33,7 +33,20 @@ interface Props {
}

export default function ProjectCard({ project }: Props) {
const links = sortBy(project.links, 'type');
const { links } = project;

const orderedLinks: T.ProjectLink[] = orderLinks(
links.map((link, i) => {
const { href, name, type } = link;
return {
id: `link-${i}`,
name,
type,
url: href
};
})
);

const { lastAnalysisDate } = project;

return (
@@ -72,20 +85,11 @@ export default function ProjectCard({ project }: Props) {
<Link to={{ pathname: '/dashboard', query: { id: project.key } }}>{project.name}</Link>
</h3>

{links.length > 0 && (
{orderedLinks.length > 0 && (
<div className="account-project-links">
<ul className="list-inline">
{links.map(link => (
<li key={link.type}>
<a
className="link-with-icon"
href={link.href}
rel="nofollow"
target="_blank"
title={link.name}>
<ProjectLinkIcon type={link.type} />
</a>
</li>
{orderedLinks.map(link => (
<MetaLink iconOnly={true} key={link.id} link={link} />
))}
</ul>
</div>

+ 25
- 33
server/sonar-web/src/main/js/apps/account/projects/__tests__/ProjectCard-test.tsx View File

@@ -19,68 +19,60 @@
*/
import * as React from 'react';
import { shallow } from 'enzyme';
import { Link } from 'react-router';
import ProjectCard from '../ProjectCard';
import Level from '../../../../components/ui/Level';

const BASE = { key: 'key', links: [], name: 'name' };

it('should render key and name', () => {
const project = { ...BASE };
const output = shallow(<ProjectCard project={project} />);
expect(output.find('.account-project-key').text()).toBe('key');
const wrapper = shallowRender();
expect(wrapper.find('.account-project-key').text()).toBe('key');
expect(
output
wrapper
.find('.account-project-name')
.find(Link)
.find('Link')
.prop('children')
).toBe('name');
});

it('should render description', () => {
const project = { ...BASE, description: 'bla' };
const output = shallow(<ProjectCard project={project} />);
expect(output.find('.account-project-description').text()).toBe('bla');
const wrapper = shallowRender({ description: 'bla' });
expect(wrapper.find('.account-project-description').text()).toBe('bla');
});

it('should not render optional fields', () => {
const project = { ...BASE };
const output = shallow(<ProjectCard project={project} />);
expect(output.find('.account-project-description').length).toBe(0);
expect(output.find('.account-project-quality-gate').length).toBe(0);
expect(output.find('.account-project-links').length).toBe(0);
const wrapper = shallowRender();
expect(wrapper.find('.account-project-description').length).toBe(0);
expect(wrapper.find('.account-project-quality-gate').length).toBe(0);
expect(wrapper.find('.account-project-links').length).toBe(0);
});

it('should render analysis date', () => {
const project = { ...BASE, lastAnalysisDate: '2016-05-17' };
const output = shallow(<ProjectCard project={project} />);
expect(output.find('.account-project-analysis DateFromNow')).toHaveLength(1);
const wrapper = shallowRender({ lastAnalysisDate: '2016-05-17' });
expect(wrapper.find('.account-project-analysis DateFromNow')).toHaveLength(1);
});

it('should not render analysis date', () => {
const project = { ...BASE };
const output = shallow(<ProjectCard project={project} />);
expect(output.find('.account-project-analysis').text()).toContain(
const wrapper = shallowRender();
expect(wrapper.find('.account-project-analysis').text()).toContain(
'my_account.projects.never_analyzed'
);
});

it('should render quality gate status', () => {
const project = { ...BASE, qualityGate: 'ERROR' };
const output = shallow(<ProjectCard project={project} />);
const wrapper = shallowRender({ qualityGate: 'ERROR' });
expect(
output
wrapper
.find('.account-project-quality-gate')
.find(Level)
.find('Level')
.prop('level')
).toBe('ERROR');
});

it('should render links', () => {
const project = {
...BASE,
links: [{ name: 'n', type: 't', href: 'h' }]
};
const output = shallow(<ProjectCard project={project} />);
expect(output.find('.account-project-links').find('li').length).toBe(1);
const wrapper = shallowRender({
links: [{ name: 'name', type: 'type', href: 'href' }]
});
expect(wrapper.find('MetaLink').length).toBe(1);
});

function shallowRender(project: Partial<T.MyProject> = {}) {
return shallow(<ProjectCard project={{ key: 'key', links: [], name: 'name', ...project }} />);
}

+ 35
- 0
server/sonar-web/src/main/js/apps/overview/meta/MetaLink.css View File

@@ -0,0 +1,35 @@
/*
* SonarQube
* Copyright (C) 2009-2019 SonarSource SA
* mailto:info AT sonarsource DOT com
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 3 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
.copy-paste-link .overview-key {
width: 80%;
}

.copy-paste-link .close {
color: #000;
border-bottom: 0;
height: 100%;
display: inline-block;
margin-left: 5px;
box-sizing: border-box;
}

.copy-paste-link .close svg {
vertical-align: sub;
}

+ 7
- 3
server/sonar-web/src/main/js/apps/overview/meta/MetaLink.tsx View File

@@ -22,8 +22,10 @@ import { getLinkName } from '../../projectLinks/utils';
import ProjectLinkIcon from '../../../components/icons-components/ProjectLinkIcon';
import isValidUri from '../../../app/utils/isValidUri';
import ClearIcon from '../../../components/icons-components/ClearIcon';
import './MetaLink.css';

interface Props {
iconOnly?: boolean;
link: T.ProjectLink;
}

@@ -42,7 +44,8 @@ export default class MetaLink extends React.PureComponent<Props, State> {
};

render() {
const { link } = this.props;
const { iconOnly, link } = this.props;
const linkTitle = getLinkName(link);
return (
<li>
<a
@@ -50,9 +53,10 @@ export default class MetaLink extends React.PureComponent<Props, State> {
href={link.url}
onClick={!isValidUri(link.url) ? this.handleClick : undefined}
rel="nofollow noreferrer noopener"
target="_blank">
target="_blank"
title={linkTitle}>
<ProjectLinkIcon className="little-spacer-right" type={link.type} />
{getLinkName(link)}
{!iconOnly && linkTitle}
</a>
{this.state.expanded && (
<div className="little-spacer-top copy-paste-link">

+ 1
- 0
server/sonar-web/src/main/js/apps/overview/meta/__tests__/MetaLink-test.tsx View File

@@ -31,6 +31,7 @@ it('should match snapshot', () => {
};

expect(shallow(<MetaLink link={link} />)).toMatchSnapshot();
expect(shallow(<MetaLink iconOnly={true} link={link} />)).toMatchSnapshot();
});

it('should render dangerous links as plaintext', () => {

+ 22
- 0
server/sonar-web/src/main/js/apps/overview/meta/__tests__/__snapshots__/MetaLink-test.tsx.snap View File

@@ -7,6 +7,7 @@ exports[`should expand and collapse link 1`] = `
href="scm:git:git@github.com"
rel="nofollow noreferrer noopener"
target="_blank"
title="Foo"
>
<ProjectLinkIcon
className="little-spacer-right"
@@ -24,6 +25,7 @@ exports[`should expand and collapse link 2`] = `
href="scm:git:git@github.com"
rel="nofollow noreferrer noopener"
target="_blank"
title="Foo"
>
<ProjectLinkIcon
className="little-spacer-right"
@@ -41,6 +43,7 @@ exports[`should expand and collapse link 3`] = `
href="scm:git:git@github.com"
rel="nofollow noreferrer noopener"
target="_blank"
title="Foo"
>
<ProjectLinkIcon
className="little-spacer-right"
@@ -58,6 +61,7 @@ exports[`should match snapshot 1`] = `
href="http://example.com"
rel="nofollow noreferrer noopener"
target="_blank"
title="Foo"
>
<ProjectLinkIcon
className="little-spacer-right"
@@ -68,6 +72,23 @@ exports[`should match snapshot 1`] = `
</li>
`;

exports[`should match snapshot 2`] = `
<li>
<a
className="link-with-icon"
href="http://example.com"
rel="nofollow noreferrer noopener"
target="_blank"
title="Foo"
>
<ProjectLinkIcon
className="little-spacer-right"
type="foo"
/>
</a>
</li>
`;

exports[`should render dangerous links as plaintext 1`] = `
<li>
<a
@@ -76,6 +97,7 @@ exports[`should render dangerous links as plaintext 1`] = `
onClick={[Function]}
rel="nofollow noreferrer noopener"
target="_blank"
title="Dangerous"
>
<ProjectLinkIcon
className="little-spacer-right"

+ 0
- 17
server/sonar-web/src/main/js/apps/overview/styles.css View File

@@ -474,23 +474,6 @@
background-color: transparent !important;
}

.copy-paste-link .overview-key {
width: 90%;
}

.copy-paste-link .close {
color: #000;
border-bottom: 0;
height: 100%;
display: inline-block;
margin-left: 5px;
box-sizing: border-box;
}

.copy-paste-link .close svg {
vertical-align: sub;
}

.overview-deleted-profile,
.overview-deprecated-rules {
margin: 4px -6px 4px;

+ 1
- 1
server/sonar-web/src/main/js/apps/projectLinks/utils.ts View File

@@ -31,7 +31,7 @@ export function orderLinks<T extends NameAndType>(links: T[]) {
const [provided, unknown] = partition<T>(links, isProvided);
return [
...sortBy(provided, link => PROVIDED_TYPES.indexOf(link.type)),
...sortBy(unknown, link => link.name!.toLowerCase())
...sortBy(unknown, link => link.name && link.name.toLowerCase())
];
}


Loading…
Cancel
Save