]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-11867, SSF-74 Fix XSS in project links on account/projects (#3203)
authorWouter Admiraal <45544358+wouter-admiraal-sonarsource@users.noreply.github.com>
Fri, 29 Mar 2019 08:09:29 +0000 (09:09 +0100)
committerGitHub <noreply@github.com>
Fri, 29 Mar 2019 08:09:29 +0000 (09:09 +0100)
server/sonar-web/src/main/js/apps/account/projects/ProjectCard.tsx
server/sonar-web/src/main/js/apps/account/projects/__tests__/ProjectCard-test.js
server/sonar-web/src/main/js/apps/overview/meta/MetaLink.d.ts [new file with mode: 0644]
server/sonar-web/src/main/js/apps/overview/meta/MetaLink.js
server/sonar-web/src/main/js/apps/overview/meta/__tests__/MetaLink-test.js
server/sonar-web/src/main/js/apps/overview/meta/__tests__/__snapshots__/MetaLink-test.js.snap
server/sonar-web/src/main/js/apps/project-admin/links/utils.js

index cfa10ff8e3703926562861eb1e57a8293b3efe0c..a0d2959306a383832654b5e4aa0301cfb86510df 100644 (file)
  * 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 { Project } from './types';
 import DateFromNow from '../../../components/intl/DateFromNow';
 import DateTimeFormatter from '../../../components/intl/DateTimeFormatter';
 import Level from '../../../components/ui/Level';
 import Tooltip from '../../../components/controls/Tooltip';
+import MetaLink from '../../overview/meta/MetaLink';
+import { orderLinks } from '../../project-admin/links/utils';
 import { translateWithParameters, translate } from '../../../helpers/l10n';
 
 interface Props {
   project: Project;
 }
 
+type ProjectLink = {
+  id: string;
+  name: string;
+  url: string;
+  type: string;
+};
+
 export default function ProjectCard({ project }: Props) {
   const isAnalyzed = project.lastAnalysisDate != null;
-  const links = sortBy(project.links, 'type');
+
+  const { links } = project;
+  const orderedLinks: ProjectLink[] = orderLinks(
+    links.map((link, i) => {
+      const { href, name, type } = link;
+      return {
+        id: `link-${i}`,
+        name,
+        type,
+        url: href
+      };
+    })
+  );
 
   return (
     <div className="account-project-card clearfix">
@@ -70,17 +90,8 @@ export default function ProjectCard({ project }: Props) {
       {links.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}
-                  title={link.name}
-                  target="_blank"
-                  rel="nofollow">
-                  <i className={`icon-color-link icon-${link.type}`} />
-                </a>
-              </li>
+            {orderedLinks.map(link => (
+              <MetaLink iconOnly={true} key={link.id} link={link} />
             ))}
           </ul>
         </div>
index c714da617dc9f447d3ee562c8e9fbe1f9730620b..997c6acd5057016ce3e826a38ddf122bc86fe3db 100644 (file)
@@ -82,5 +82,5 @@ it('should render links', () => {
     links: [{ name: 'n', type: 't', href: 'h' }]
   };
   const output = shallow(<ProjectCard project={project} />);
-  expect(output.find('.account-project-links').find('li').length).toBe(1);
+  expect(output.find('MetaLink').length).toBe(1);
 });
diff --git a/server/sonar-web/src/main/js/apps/overview/meta/MetaLink.d.ts b/server/sonar-web/src/main/js/apps/overview/meta/MetaLink.d.ts
new file mode 100644 (file)
index 0000000..963030c
--- /dev/null
@@ -0,0 +1,34 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2018 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.
+ */
+import * as React from 'react';
+
+type Link = {
+  id: string;
+  name: string;
+  url: string;
+  type: string;
+};
+
+interface Props {
+  iconOnly?: boolean;
+  link: Link;
+}
+
+export default class MetaLink extends React.Component<Props> {}
index 2f368484c9bf953780663307a7729f354eafa2c4..08d1c4db53ce322c2e1db9045540c6643fb80186 100644 (file)
@@ -39,6 +39,7 @@ type State = {|
 
 export default class MetaLink extends React.PureComponent {
   /*:: props: {
+    iconOnly?: boolean,
     link: Link
   };
 */
@@ -66,7 +67,7 @@ export default class MetaLink extends React.PureComponent {
   }
 
   render() {
-    const { link } = this.props;
+    const { iconOnly, link } = this.props;
 
     return (
       <li>
@@ -74,10 +75,10 @@ export default class MetaLink extends React.PureComponent {
           className="link-with-icon"
           href={link.url}
           target="_blank"
-          onClick={!isClickable(link) && this.handleClick}>
+          onClick={!isClickable(link) && this.handleClick}
+          title={link.name}>
           {this.renderLinkIcon(link)}
-          &nbsp;
-          {link.name}
+          {!iconOnly && `\u00A0${link.name}`}
         </a>
         {this.state.expanded && (
           <div className="little-spacer-top">
index 2499c2e3c365b45e42c579444ff96e12c4ac772c..3af59d19e938b4cfd70129567a4e0cad05850924 100644 (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 expand and collapse link', () => {
index 416a8104ae2341ff65eed8952fa2af882c82d506..931f9d3f9c1349f79de5a7ca92a1af6150c652f0 100644 (file)
@@ -7,12 +7,12 @@ exports[`should expand and collapse link 1`] = `
     href="scm:git:git@github.com"
     onClick={[Function]}
     target="_blank"
+    title="Foo"
   >
     <i
       className="icon-color-link icon-detach"
     />
-     
-    Foo
+     Foo
   </a>
 </li>
 `;
@@ -24,12 +24,12 @@ exports[`should expand and collapse link 2`] = `
     href="scm:git:git@github.com"
     onClick={[Function]}
     target="_blank"
+    title="Foo"
   >
     <i
       className="icon-color-link icon-detach"
     />
-     
-    Foo
+     Foo
   </a>
   <div
     className="little-spacer-top"
@@ -52,12 +52,12 @@ exports[`should expand and collapse link 3`] = `
     href="scm:git:git@github.com"
     onClick={[Function]}
     target="_blank"
+    title="Foo"
   >
     <i
       className="icon-color-link icon-detach"
     />
-     
-    Foo
+     Foo
   </a>
 </li>
 `;
@@ -69,12 +69,28 @@ exports[`should match snapshot 1`] = `
     href="http://example.com"
     onClick={false}
     target="_blank"
+    title="Foo"
+  >
+    <i
+      className="icon-color-link icon-detach"
+    />
+     Foo
+  </a>
+</li>
+`;
+
+exports[`should match snapshot 2`] = `
+<li>
+  <a
+    className="link-with-icon"
+    href="http://example.com"
+    onClick={false}
+    target="_blank"
+    title="Foo"
   >
     <i
       className="icon-color-link icon-detach"
     />
-     
-    Foo
   </a>
 </li>
 `;
index 9ae5e6ef3d7356fe561cff4a602b308cd87f870c..c8772b917cc707bb3119c150d847988c31fba385 100644 (file)
@@ -29,7 +29,7 @@ export function orderLinks(links) {
   const [provided, unknown] = partition(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())
   ];
 }