]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-11506, SSF-62 Handle XSS code in project links
authorWouter Admiraal <wouter.admiraal@sonarsource.com>
Tue, 11 Dec 2018 07:35:04 +0000 (08:35 +0100)
committersonartech <sonartech@sonarsource.com>
Thu, 20 Dec 2018 10:41:28 +0000 (11:41 +0100)
server/sonar-web/src/main/js/app/utils/isValidUri.ts [new file with mode: 0644]
server/sonar-web/src/main/js/apps/overview/meta/MetaLink.tsx
server/sonar-web/src/main/js/apps/overview/meta/__tests__/MetaLink-test.tsx
server/sonar-web/src/main/js/apps/overview/meta/__tests__/__snapshots__/MetaLink-test.tsx.snap
server/sonar-web/src/main/js/apps/overview/styles.css
server/sonar-web/src/main/js/apps/projectLinks/LinkRow.tsx
server/sonar-web/src/main/js/apps/projectLinks/__tests__/LinkRow-test.tsx
server/sonar-web/src/main/js/apps/projectLinks/__tests__/__snapshots__/LinkRow-test.tsx.snap

diff --git a/server/sonar-web/src/main/js/app/utils/isValidUri.ts b/server/sonar-web/src/main/js/app/utils/isValidUri.ts
new file mode 100644 (file)
index 0000000..115eaf6
--- /dev/null
@@ -0,0 +1,24 @@
+/*
+ * 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 { isWebUri } from 'valid-url';
+
+export default function(url: string): boolean {
+  return /^(\/|scm:)/.test(url) || !!isWebUri(url);
+}
index a1e1e6bad13d8e7009acc30561140f76b22b49fc..4c20250f881be06f0bd3bbfbe66941b0a6d8326f 100644 (file)
 import * as React from 'react';
 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';
 
 interface Props {
   link: T.ProjectLink;
 }
 
-export default function MetaLink({ link }: Props) {
-  return (
-    <li>
-      <a className="link-with-icon" href={link.url} rel="nofollow" target="_blank">
-        <ProjectLinkIcon className="little-spacer-right" type={link.type} />
-        {getLinkName(link)}
-      </a>
-    </li>
-  );
+interface State {
+  expanded: boolean;
+}
+
+export default class MetaLink extends React.PureComponent<Props, State> {
+  state = {
+    expanded: false
+  };
+
+  handleClick = (event: React.MouseEvent<HTMLAnchorElement>) => {
+    event.preventDefault();
+    this.setState(s => ({ expanded: !s.expanded }));
+  };
+
+  render() {
+    const { link } = this.props;
+    return (
+      <li>
+        <a
+          className="link-with-icon"
+          href={link.url}
+          onClick={!isValidUri(link.url) ? this.handleClick : undefined}
+          rel="nofollow noreferrer noopener"
+          target="_blank">
+          <ProjectLinkIcon className="little-spacer-right" type={link.type} />
+          {getLinkName(link)}
+        </a>
+        {this.state.expanded && (
+          <div className="little-spacer-top copy-paste-link">
+            <input
+              className="overview-key"
+              onClick={(event: React.MouseEvent<HTMLInputElement>) => event.currentTarget.select()}
+              readOnly={true}
+              type="text"
+              value={link.url}
+            />
+            <a className="close" href="#" onClick={this.handleClick}>
+              <ClearIcon />
+            </a>
+          </div>
+        )}
+      </li>
+    );
+  }
 }
index 0bea70841d822d84759c5d72808b5256855d8157..23d0d3fa5eb77f261c4769c0635bb470e5c9368e 100644 (file)
@@ -33,6 +33,17 @@ it('should match snapshot', () => {
   expect(shallow(<MetaLink link={link} />)).toMatchSnapshot();
 });
 
+it('should render dangerous links as plaintext', () => {
+  const link = {
+    id: '1',
+    name: 'Dangerous',
+    url: 'javascript:alert("hi")',
+    type: 'dangerous'
+  };
+
+  expect(shallow(<MetaLink link={link} />)).toMatchSnapshot();
+});
+
 it('should expand and collapse link', () => {
   const link = {
     id: '1',
index 553a37c7bf1c98f23b683980ac8bccae3d3f1375..a8f7dbb5bbaf750139dab59271c6471fa0f1bf09 100644 (file)
@@ -5,7 +5,7 @@ exports[`should expand and collapse link 1`] = `
   <a
     className="link-with-icon"
     href="scm:git:git@github.com"
-    rel="nofollow"
+    rel="nofollow noreferrer noopener"
     target="_blank"
   >
     <ProjectLinkIcon
@@ -22,7 +22,7 @@ exports[`should expand and collapse link 2`] = `
   <a
     className="link-with-icon"
     href="scm:git:git@github.com"
-    rel="nofollow"
+    rel="nofollow noreferrer noopener"
     target="_blank"
   >
     <ProjectLinkIcon
@@ -39,7 +39,7 @@ exports[`should expand and collapse link 3`] = `
   <a
     className="link-with-icon"
     href="scm:git:git@github.com"
-    rel="nofollow"
+    rel="nofollow noreferrer noopener"
     target="_blank"
   >
     <ProjectLinkIcon
@@ -56,7 +56,7 @@ exports[`should match snapshot 1`] = `
   <a
     className="link-with-icon"
     href="http://example.com"
-    rel="nofollow"
+    rel="nofollow noreferrer noopener"
     target="_blank"
   >
     <ProjectLinkIcon
@@ -67,3 +67,21 @@ exports[`should match snapshot 1`] = `
   </a>
 </li>
 `;
+
+exports[`should render dangerous links as plaintext 1`] = `
+<li>
+  <a
+    className="link-with-icon"
+    href="javascript:alert(\\"hi\\")"
+    onClick={[Function]}
+    rel="nofollow noreferrer noopener"
+    target="_blank"
+  >
+    <ProjectLinkIcon
+      className="little-spacer-right"
+      type="dangerous"
+    />
+    Dangerous
+  </a>
+</li>
+`;
index 06480c796cdc473e8d73d1177d48be309d157be0..2ded3d250d6f95d91cc699c9c5f9f8d31f91830c 100644 (file)
   background-color: transparent !important;
 }
 
+.copy-paste-link .overview-key {
+  width: 90%;
+}
+
+.copy-paste-link .close {
+  color: black;
+  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;
index 9ca2a8f690650fd49196c0d119f95a228a1556d3..c3f83b718f6cdc65485e14fe31563ce51b4311eb 100644 (file)
@@ -23,6 +23,7 @@ import ConfirmButton from '../../components/controls/ConfirmButton';
 import ProjectLinkIcon from '../../components/icons-components/ProjectLinkIcon';
 import { Button } from '../../components/ui/buttons';
 import { translate, translateWithParameters } from '../../helpers/l10n';
+import isValidUri from '../../app/utils/isValidUri';
 
 interface Props {
   link: T.ProjectLink;
@@ -90,9 +91,13 @@ export default class LinkRow extends React.PureComponent<Props> {
       <tr data-name={link.name}>
         <td className="nowrap">{this.renderName(link)}</td>
         <td className="nowrap js-url">
-          <a href={link.url} rel="nofollow" target="_blank">
-            {link.url}
-          </a>
+          {isValidUri(link.url) ? (
+            <a href={link.url} rel="nofollow noreferrer noopener" target="_blank">
+              {link.url}
+            </a>
+          ) : (
+            link.url
+          )}
         </td>
         <td className="thin nowrap">{this.renderDeleteButton(link)}</td>
       </tr>
index 19cff23dd6210b90bfa3208be3710d1462d09356..3c93017c8aedbdf401cd1195286e624f945abe04 100644 (file)
@@ -42,3 +42,14 @@ it('should render custom link', () => {
     )
   ).toMatchSnapshot();
 });
+
+it('should render dangerous code as plain text', () => {
+  expect(
+    shallow(
+      <LinkRow
+        link={{ id: '12', name: 'dangerous', type: 'dangerous', url: 'javascript:alert("Hello")' }}
+        onDelete={jest.fn()}
+      />
+    )
+  ).toMatchSnapshot();
+});
index c76c3e13aef07903103ad1cdbffa8f90c33e2a14..e05b8f94f914a536146e86db5037e4e6290bfa0e 100644 (file)
@@ -28,7 +28,7 @@ exports[`should render custom link 1`] = `
   >
     <a
       href="http://example.com"
-      rel="nofollow"
+      rel="nofollow noreferrer noopener"
       target="_blank"
     >
       http://example.com
@@ -51,6 +51,51 @@ exports[`should render custom link 1`] = `
 </tr>
 `;
 
+exports[`should render dangerous code as plain text 1`] = `
+<tr
+  data-name="dangerous"
+>
+  <td
+    className="nowrap"
+  >
+    <div>
+      <ProjectLinkIcon
+        className="little-spacer-right"
+        type="dangerous"
+      />
+      <div
+        className="display-inline-block text-top"
+      >
+        <span
+          className="js-name"
+        >
+          dangerous
+        </span>
+      </div>
+    </div>
+  </td>
+  <td
+    className="nowrap js-url"
+  >
+    javascript:alert("Hello")
+  </td>
+  <td
+    className="thin nowrap"
+  >
+    <ConfirmButton
+      confirmButtonText="delete"
+      confirmData="12"
+      isDestructive={true}
+      modalBody="project_links.are_you_sure_to_delete_x_link.dangerous"
+      modalHeader="project_links.delete_project_link"
+      onConfirm={[MockFunction]}
+    >
+      <Component />
+    </ConfirmButton>
+  </td>
+</tr>
+`;
+
 exports[`should render provided link 1`] = `
 <tr>
   <td
@@ -88,7 +133,7 @@ exports[`should render provided link 1`] = `
   >
     <a
       href="http://example.com"
-      rel="nofollow"
+      rel="nofollow noreferrer noopener"
       target="_blank"
     >
       http://example.com