Mike Samuel 0895052c01 Polygerrit now loads polymer-resin
polymer-resin intercepts polymer property assignments
before they reach XSS-vulnerable sinks like `href="..."`
and text nodes in `<script>` elements.

This follows the instructions in WORKSPACE for adding a new bower
dependency with kaspern's tweak to use the dependency in a rule so
that it's found.  //lib/js/bower_components.bzl has already been
rolled-back per those instructions.

The license is the polymer license as can be seen at
https://github.com/Polymer/polymer-resin/blob/master/LICENSE though
I'm not sure that //tools/js/bower2bazel.py recognizes it as such.

Docs for the added component are available at
https://github.com/Polymer/polymer-resin/blob/master/README.md
https://github.com/Polymer/polymer-resin/blob/master/getting-started.md

With this change, when I introduce an XSS vulnerability as below,
polymer-resin intercepts and stops it.

Patch that introduces a strawman vulnerability.

--- a/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header.js
+++ b/polygerrit-ui/app/elements/core/gr-main-header/gr-main-header.js
@@ -55,6 +55,10 @@
         url: '/q/status:abandoned',
         name: 'Abandoned',
       },
+      {
+        url: location.hash.replace(/^#/, '') || 'http://example.com/#fragment_echoed_here',
+        name: 'XSS Me',
+      },
     ],
   }];

---

Address kaspern's and paladox's comments.

---

Undo version bumps for bower dependencies.

---

Change Soy index template to parallel app/index.html.

---

update polymer-resin to version 1.1.1-beta

----

Load polymer-resin into polygerrit-ui/**/*_test.html

After this, I ran the tests with
  -l chrome
  -l firefox

I ran a handful of tests with -p and observed that the
console shows "initResin" is called before test cases start
executing.

These changes were done programmaticly by running the script below
(approximately) thus:
```
gerrit/ $ cd polygerrit-ui/app
app/ $ find . -name \*test.html | xargs perl hack-tests.pl
```

```
use strict;

sub removeResin($) {
  my $s = $_[0];
  $s =~ s@<link rel="import" href="[^"]*/polymer-resin/[^"]*"[^>]*>\n?@@;
  $s =~ s@<script src="[^"]*/polymer-resin/[^"]*"></script>\n?@@;
  $s =~ s@<script>\s*security\.polymer_resin.*?</script>\n?@@s;
  return $s;
}

for my $f (@ARGV) {
  next if $f =~ m@/bower_components/|/node_modules/@;

  system('git', 'checkout', $f);
  print "$f\n";

  my @lines = ();
  open(IN, "<$f") or die "$f: $!";
  my $maxLineOfMatch = 0;
  while (<IN>) {
    push(@lines, $_);
    # Put a marker after core loading directives.
    $maxLineOfMatch = scalar(@lines)
      if m@/webcomponentsjs/|/polymer[.]html\b|/browser[.]js@;
  }
  close(IN) or die "$f: $!";

  die "$f missing loading directives" unless $maxLineOfMatch;

  # Given ./a/b/c/my_test.html, $pathToRoot is "../../.."
  # assuming no non-leading . or .. components in the path from find.
  my $pathToRoot = $f;
  $pathToRoot =~ s@^\.\/@@;
  $pathToRoot =~ s@^(.*?/)?app/@@;
  $pathToRoot =~ s@\/[^\/]*$@@;
  $pathToRoot =~ s@[^/]+@..@g;

  my $nLines = scalar(@lines);
  open(OUT, ">$f") or die "$f: $!";

  # Output the lines up to the last polymer-resin dependency
  # loaded explicitly by this test.
  my $before = join '', @lines[0..($maxLineOfMatch - 1)];
  $before = removeResin($before);
  print OUT "$before";

  # Dump out the lines that load polymer-resin and configure it for
  # polygerrit.
  if (1) {
      print OUT qq'<link rel="import" href="$pathToRoot/bower_components/polymer-resin/standalone/polymer-resin-debug.html"/>
<script>
security.polymer_resin.install({allowedIdentifierPrefixes: [\'\']});
</script>
    ';
  }

  # Emit any remaining lines.
  my $after = join '', @lines[$maxLineOfMatch..$#lines];
  $after = removeResin($after);
  $after =~ s/^\n*//;
  print OUT "$after";

  close(OUT) or die "$f: $!";
}
```

---

update polymer-resin to version 1.2.1-beta

---

update Soy index template to new style polymer-resin initialization

----

fix lint warnings

----

Load test/common-test-setup.html into *_test.html

Instead of inserting instructions to load and initialize polymer-resin into
every test file, add a common-test-setup.html that does that and also fold
iron-test-helpers loading into it.

----

imported files do not need to load webcomponentsjs

Change-Id: I71221c36ed8a0fe7f8720c1064a2fcc9555bb8df
2017-05-30 23:16:09 -04:00

484 lines
16 KiB
HTML

<!DOCTYPE html>
<!--
Copyright (C) 2016 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">
<title>gr-syntax-layer</title>
<script src="../../../bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
<script src="../../../bower_components/web-component-tester/browser.js"></script>
<link rel="import" href="../../../test/common-test-setup.html"/>
<link rel="import" href="../../shared/gr-rest-api-interface/mock-diff-response_test.html">
<link rel="import" href="gr-syntax-layer.html">
<script>void(0);</script>
<test-fixture id="basic">
<template>
<gr-syntax-layer></gr-syntax-layer>
</template>
</test-fixture>
<script>
suite('gr-syntax-layer tests', () => {
let sandbox;
let diff;
let element;
function getMockHLJS() {
const html = '<span class="gr-diff gr-syntax gr-syntax-string">' +
'ipsum</span>';
return {
configure() {},
highlight(lang, line, ignore, state) {
return {
value: line.replace(/ipsum/, html),
top: state === undefined ? 1 : state + 1,
};
},
};
}
setup(() => {
sandbox = sinon.sandbox.create();
element = fixture('basic');
const mock = document.createElement('mock-diff-response');
diff = mock.diffResponse;
element.diff = diff;
});
teardown(() => {
sandbox.restore();
});
test('annotate without range does nothing', () => {
const annotationSpy = sandbox.spy(GrAnnotation, 'annotateElement');
const el = document.createElement('div');
el.textContent = 'Etiam dui, blandit wisi.';
const line = new GrDiffLine(GrDiffLine.Type.REMOVE);
line.beforeNumber = 12;
element.annotate(el, line);
assert.isFalse(annotationSpy.called);
});
test('annotate with range applies it', () => {
const str = 'Etiam dui, blandit wisi.';
const start = 6;
const length = 3;
const className = 'foobar';
const annotationSpy = sandbox.spy(GrAnnotation, 'annotateElement');
const el = document.createElement('div');
el.textContent = str;
const line = new GrDiffLine(GrDiffLine.Type.REMOVE);
line.beforeNumber = 12;
element._baseRanges[11] = [{
start,
length,
className,
}];
element.annotate(el, line);
assert.isTrue(annotationSpy.called);
assert.equal(annotationSpy.lastCall.args[0], el);
assert.equal(annotationSpy.lastCall.args[1], start);
assert.equal(annotationSpy.lastCall.args[2], length);
assert.equal(annotationSpy.lastCall.args[3], className);
assert.isOk(el.querySelector('hl.' + className));
});
test('annotate with range but disabled does nothing', () => {
const str = 'Etiam dui, blandit wisi.';
const start = 6;
const length = 3;
const className = 'foobar';
const annotationSpy = sandbox.spy(GrAnnotation, 'annotateElement');
const el = document.createElement('div');
el.textContent = str;
const line = new GrDiffLine(GrDiffLine.Type.REMOVE);
line.beforeNumber = 12;
element._baseRanges[11] = [{
start,
length,
className,
}];
element.enabled = false;
element.annotate(el, line);
assert.isFalse(annotationSpy.called);
});
test('process on empty diff does nothing', done => {
element.diff = {
meta_a: {content_type: 'application/json'},
meta_b: {content_type: 'application/json'},
content: [],
};
const processNextSpy = sandbox.spy(element, '_processNextLine');
const processPromise = element.process();
processPromise.then(() => {
assert.isFalse(processNextSpy.called);
assert.equal(element._baseRanges.length, 0);
assert.equal(element._revisionRanges.length, 0);
done();
});
});
test('process for unsupported languages does nothing', done => {
element.diff = {
meta_a: {content_type: 'text/x+objective-cobol-plus-plus'},
meta_b: {content_type: 'application/not-a-real-language'},
content: [],
};
const processNextSpy = sandbox.spy(element, '_processNextLine');
const processPromise = element.process();
processPromise.then(() => {
assert.isFalse(processNextSpy.called);
assert.equal(element._baseRanges.length, 0);
assert.equal(element._revisionRanges.length, 0);
done();
});
});
test('process while disabled does nothing', done => {
const processNextSpy = sandbox.spy(element, '_processNextLine');
element.enabled = false;
const loadHLJSSpy = sandbox.spy(element, '_loadHLJS');
const processPromise = element.process();
processPromise.then(() => {
assert.isFalse(processNextSpy.called);
assert.equal(element._baseRanges.length, 0);
assert.equal(element._revisionRanges.length, 0);
assert.isFalse(loadHLJSSpy.called);
done();
});
});
test('process highlight ipsum', done => {
element.diff.meta_a.content_type = 'application/json';
element.diff.meta_b.content_type = 'application/json';
const mockHLJS = getMockHLJS();
const highlightSpy = sinon.spy(mockHLJS, 'highlight');
sandbox.stub(element.$.libLoader, 'get',
() => { return Promise.resolve(mockHLJS); });
const processNextSpy = sandbox.spy(element, '_processNextLine');
const processPromise = element.process();
processPromise.then(() => {
const linesA = diff.meta_a.lines;
const linesB = diff.meta_b.lines;
assert.isTrue(processNextSpy.called);
assert.equal(element._baseRanges.length, linesA);
assert.equal(element._revisionRanges.length, linesB);
assert.equal(highlightSpy.callCount, linesA + linesB);
// The first line of both sides have a range.
let ranges = [element._baseRanges[0], element._revisionRanges[0]];
for (const range of ranges) {
assert.equal(range.length, 1);
assert.equal(range[0].className,
'gr-diff gr-syntax gr-syntax-string');
assert.equal(range[0].start, 'lorem '.length);
assert.equal(range[0].length, 'ipsum'.length);
}
// There are no ranges from ll.1-12 on the left and ll.1-11 on the
// right.
ranges = element._baseRanges.slice(1, 12)
.concat(element._revisionRanges.slice(1, 11));
for (const range of ranges) {
assert.equal(range.length, 0);
}
// There should be another pair of ranges on l.13 for the left and
// l.12 for the right.
ranges = [element._baseRanges[13], element._revisionRanges[12]];
for (const range of ranges) {
assert.equal(range.length, 1);
assert.equal(range[0].className,
'gr-diff gr-syntax gr-syntax-string');
assert.equal(range[0].start, 32);
assert.equal(range[0].length, 'ipsum'.length);
}
// The next group should have a similar instance on either side.
let range = element._baseRanges[15];
assert.equal(range.length, 1);
assert.equal(range[0].className, 'gr-diff gr-syntax gr-syntax-string');
assert.equal(range[0].start, 34);
assert.equal(range[0].length, 'ipsum'.length);
range = element._revisionRanges[14];
assert.equal(range.length, 1);
assert.equal(range[0].className, 'gr-diff gr-syntax gr-syntax-string');
assert.equal(range[0].start, 35);
assert.equal(range[0].length, 'ipsum'.length);
done();
});
});
test('_diffChanged calls cancel', () => {
const cancelSpy = sandbox.spy(element, '_diffChanged');
element.diff = {content: []};
assert.isTrue(cancelSpy.called);
});
test('_rangesFromElement no ranges', () => {
const elem = document.createElement('span');
elem.textContent = 'Etiam dui, blandit wisi.';
const offset = 100;
const result = element._rangesFromElement(elem, offset);
assert.equal(result.length, 0);
});
test('_rangesFromElement single range', () => {
const str0 = 'Etiam ';
const str1 = 'dui, blandit';
const str2 = ' wisi.';
const className = 'gr-diff gr-syntax gr-syntax-string';
const offset = 100;
const elem = document.createElement('span');
elem.appendChild(document.createTextNode(str0));
const span = document.createElement('span');
span.textContent = str1;
span.className = className;
elem.appendChild(span);
elem.appendChild(document.createTextNode(str2));
const result = element._rangesFromElement(elem, offset);
assert.equal(result.length, 1);
assert.equal(result[0].start, str0.length + offset);
assert.equal(result[0].length, str1.length);
assert.equal(result[0].className, className);
});
test('_rangesFromElement non-whitelist', () => {
const str0 = 'Etiam ';
const str1 = 'dui, blandit';
const str2 = ' wisi.';
const className = 'not-in-the-whitelist';
const offset = 100;
const elem = document.createElement('span');
elem.appendChild(document.createTextNode(str0));
const span = document.createElement('span');
span.textContent = str1;
span.className = className;
elem.appendChild(span);
elem.appendChild(document.createTextNode(str2));
const result = element._rangesFromElement(elem, offset);
assert.equal(result.length, 0);
});
test('_rangesFromElement milti range', () => {
const str0 = 'Etiam ';
const str1 = 'dui,';
const str2 = ' blandit';
const str3 = ' wisi.';
const className = 'gr-diff gr-syntax gr-syntax-string';
const offset = 100;
const elem = document.createElement('span');
elem.appendChild(document.createTextNode(str0));
let span = document.createElement('span');
span.textContent = str1;
span.className = className;
elem.appendChild(span);
elem.appendChild(document.createTextNode(str2));
span = document.createElement('span');
span.textContent = str3;
span.className = className;
elem.appendChild(span);
const result = element._rangesFromElement(elem, offset);
assert.equal(result.length, 2);
assert.equal(result[0].start, str0.length + offset);
assert.equal(result[0].length, str1.length);
assert.equal(result[0].className, className);
assert.equal(result[1].start,
str0.length + str1.length + str2.length + offset);
assert.equal(result[1].length, str3.length);
assert.equal(result[1].className, className);
});
test('_rangesFromElement nested range', () => {
const str0 = 'Etiam ';
const str1 = 'dui,';
const str2 = ' blandit';
const str3 = ' wisi.';
const className = 'gr-diff gr-syntax gr-syntax-string';
const offset = 100;
const elem = document.createElement('span');
elem.appendChild(document.createTextNode(str0));
const span1 = document.createElement('span');
span1.textContent = str1;
span1.className = className;
elem.appendChild(span1);
const span2 = document.createElement('span');
span2.textContent = str2;
span2.className = className;
span1.appendChild(span2);
elem.appendChild(document.createTextNode(str3));
const result = element._rangesFromElement(elem, offset);
assert.equal(result.length, 2);
assert.equal(result[0].start, str0.length + offset);
assert.equal(result[0].length, str1.length + str2.length);
assert.equal(result[0].className, className);
assert.equal(result[1].start, str0.length + str1.length + offset);
assert.equal(result[1].length, str2.length);
assert.equal(result[1].className, className);
});
test('_rangesFromString whitelist allows recursion', () => {
const str = [
'<span class="non-whtelisted-class">',
'<span class="gr-diff gr-syntax gr-syntax-keyword">public</span>',
'</span>'].join('');
const result = element._rangesFromString(str);
assert.notEqual(result.length, 0);
});
test('_isSectionDone', () => {
let state = {sectionIndex: 0, lineIndex: 0};
assert.isFalse(element._isSectionDone(state));
state = {sectionIndex: 0, lineIndex: 2};
assert.isFalse(element._isSectionDone(state));
state = {sectionIndex: 0, lineIndex: 4};
assert.isTrue(element._isSectionDone(state));
state = {sectionIndex: 1, lineIndex: 2};
assert.isFalse(element._isSectionDone(state));
state = {sectionIndex: 1, lineIndex: 3};
assert.isTrue(element._isSectionDone(state));
state = {sectionIndex: 3, lineIndex: 0};
assert.isFalse(element._isSectionDone(state));
state = {sectionIndex: 3, lineIndex: 3};
assert.isFalse(element._isSectionDone(state));
state = {sectionIndex: 3, lineIndex: 4};
assert.isTrue(element._isSectionDone(state));
});
test('workaround CPP LT directive', () => {
// Does nothing to regular line.
let line = 'int main(int argc, char** argv) { return 0; }';
assert.equal(element._workaround('cpp', line), line);
// Does nothing to include directive.
line = '#include <stdio>';
assert.equal(element._workaround('cpp', line), line);
// Converts left-shift operator in #define.
line = '#define GiB (1ull << 30)';
let expected = '#define GiB (1ull || 30)';
assert.equal(element._workaround('cpp', line), expected);
// Converts less-than operator in #if.
line = ' #if __GNUC__ < 4 || (__GNUC__ == 4 && __GNUC_MINOR__ < 1)';
expected = ' #if __GNUC__ | 4 || (__GNUC__ == 4 && __GNUC_MINOR__ | 1)';
assert.equal(element._workaround('cpp', line), expected);
});
test('workaround Java param-annotation', () => {
// Does nothing to regular line.
let line = 'public static void foo(int bar) { }';
assert.equal(element._workaround('java', line), line);
// Does nothing to regular annotation.
line = 'public static void foo(@Nullable int bar) { }';
assert.equal(element._workaround('java', line), line);
// Converts parameterized annotation.
line = 'public static void foo(@SuppressWarnings("unused") int bar) { }';
const expected = 'public static void foo(@SuppressWarnings "unused" ' +
' int bar) { }';
assert.equal(element._workaround('java', line), expected);
});
test('workaround CPP whcar_t character literals', () => {
// Does nothing to regular line.
let line = 'int main(int argc, char** argv) { return 0; }';
assert.equal(element._workaround('cpp', line), line);
// Does nothing to wchar_t string.
line = 'wchar_t* sz = L"abc 123";';
assert.equal(element._workaround('cpp', line), line);
// Converts wchar_t character literal to string.
line = 'wchar_t myChar = L\'#\'';
let expected = 'wchar_t myChar = L"."';
assert.equal(element._workaround('cpp', line), expected);
// Converts wchar_t character literal with escape sequence to string.
line = 'wchar_t myChar = L\'\\"\'';
expected = 'wchar_t myChar = L"\\."';
assert.equal(element._workaround('cpp', line), expected);
});
test('workaround go backslash character literals', () => {
// Does nothing to regular line.
let line = 'func foo(in []byte) (lit []byte, n int, err error) {';
assert.equal(element._workaround('go', line), line);
// Does nothing to string with backslash literal
line = 'c := "\\\\"';
assert.equal(element._workaround('go', line), line);
// Converts backslash literal character to a string.
line = 'c := \'\\\\\'';
const expected = 'c := "\\\\"';
assert.equal(element._workaround('go', line), expected);
});
});
</script>