-
Notifications
You must be signed in to change notification settings - Fork 9.3k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open in Lighthouse Viewer button #1179
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this makes me nervous :) especially since we can't really control the origin of the report. An XSS could conceivably let someone create gists in your account if you've already given permission, though that would probably rely on a handlebars exploit or one of our formatter helpers being stupid.
I don't really want to rely on us never writing stupid code, though. Should we write a lighthouse results object validator that strips out everything that doesn't match it?
'lighthouse-report.js was not inlined'); | ||
assert.ok(html.includes('.report-body {'), 'report.css was not inlined'); | ||
assert.ok(!html.includes('"lighthouseVersion'), 'lhresults were not escaped'); | ||
assert.ok(html.includes('window.lhresults = '), 'report results were inlined'); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
do you want to test more than this? (though you do test for lighthouseVersion
, below?) In the past we had a failure for a while where window.lhresults = undefined;
was passing the test
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'll check that there's an object on the page, but I don't think we should get in the business of validating the object as it's embedded on the page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
done
*/ | ||
listenForMessages() { | ||
window.addEventListener('message', e => { | ||
if (e.data.lhresults) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
not a huge increase in security, but can we check the message origin/source against the opener?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
done
}); | ||
|
||
// If the page was opened as a popup, tell the opening window we're ready. | ||
if (self.opener && !self.closed) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
self.closed
? or checking the opener
as closed?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
opener. done.
|
||
// If the page was opened as a popup, tell the opening window we're ready. | ||
if (self.opener && !self.closed) { | ||
self.opener.postMessage({opened: true}, '*'); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
again, doesn't do much, but don't we know the origin here?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
PTAL.
If you can exploit the viewer like this, it's already possible since we allow drag and drop of a user file into the app. This additional also gores through our The places where we use Update: filed #1188 to remove |
@brendankenny rebased and passing. |
27920af
to
352149d
Compare
Pingy ping. This has been open for 22 days. I think we should start with this before upstreaming viewer code into core (future PR). This PR is useful standalone. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
From a UX pov I have been thinking of something slightly different:
- User runs lighthouse
- They view the HTML report (locally)
- They want to save or share this particular report
- They click a "generate link" kind of button.
- This will open a new tab with the production report-viewer
- (User authenticates if its their first time)
- After communicating progress, the URL of the page is updated to now updated to the new permalink
- A "Copy link" link is available to automate adding to clipboard.
it's a bit of a change, so how does that UX sound to you?
{{#if script }} | ||
<script> | ||
window.lhresults = {{{lhresults}}}; | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
let's put the lhresults in a separate script tag from the {{{script}}}
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
done
is there a reason to stick with the ? |
@brendankenny i think there's two parts to that
|
@paulirish I think this will capture that entire workflow. The only thing that we'd have to change is: 4.) is "open in viewer" instead of "generate link"...but we could do a thing where if you click the share button inside a local report, it goes through your flow. @brendankenny |
* Initializes of a `message` listener to respond to postMessage events. | ||
*/ | ||
listenForMessages() { | ||
window.addEventListener('message', e => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
remove listener after a report loaded?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I can see cases where we'd want the same page to update itself again. For example, when the DT opens the tab, it could re-postMessage()
to the existing open tab.
Rebased |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
one more XSS thought, but except for better escaping this approach is looking good. It would be great to get another reviewer looking at safety before landing, though :)
@@ -19,9 +19,34 @@ | |||
|
|||
'use strict'; | |||
|
|||
function sendJSONReport() { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
add a function description comment?
|
||
{{#if scripts }} | ||
<script> | ||
window.lhresults = {{{lhresults}}}; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
would it be totally crazy to do even more defense in depth here by putting lhresults
in an html element instead of in a <script>
?
Really we should be doing more to escape our JS, but one end run around this is to do double stash {{lhresults}}
in a (display: none) div, so we get HTML entity escaping, then get the actual results when needed by JSON.parse(lhresultsDiv.textContent)
(see rule 1 and the summary section for more details)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah, so to be clear, these <script>
s (and therefore the {{{}}}
) never see user-provided input.
The flow is:
User clicks "open in viewer", the local page opens the online viewer (a page with different markup) and sends the window.lhresults
JSON to it. That page's message
listener does the replaceReportHTML
. That method then goes through our normal Handlebars pipeline to generate the inner page content.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
right, but lhresults
does see user input (which is why we have to escape any </script>
in it).
This suggestion is actually independent of the postMessage
part of this PR, and just about (safely) embedding lhresults
in the page again. Basically their advice is that if you're going to put user generated data in a page, an easy way to keep it safe is doing html escape (which the double handlebars stash does) inside an HTML element. If you want to embed the user data in a JS var literal instead there's a lot more work to do (and it doesn't appear that handlebars offers that escaping by default).
This would also let us drop our own </script>
escaping and it would leave us in a lot safer spot wrt exploits of the postMessage
system, however unlikely those are, and the change doesn't seem semantically that bad.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Really we should be doing more to escape our JS, but one end run around this is to do double stash
{{lhresults}}
in a (display: none) div, so we get HTML entity escaping, then get the actual results when needed by JSON.parse(lhresultsDiv.textContent) (see rule 1 and the summary section for more details)
I'm not sure this does anything different other than adding an additional parse. Handlebars, #1188, and #996 already force entity escaping everywhere that's needed.
The lhresults
object is also constructed and sanitized by us. It's not pure user-input. Parts of the object come from user-space but we only allow markdown (two tags at that). If the user provides HTML tags, they'll end up encoded.
This suggestion is actually independent of the postMessage part of this PR, and just about (safely) embedding lhresults in the page again.
The thing we're trying to prevent is a malicious actor creating gists in a user's account. That all happens online. Locally, I'm not sure what there is to worry about? A malicious actor would have to create a page such that running LH over it properly escapes the embedded JSON (which we prevent), alters lhresults
to their advantage, the user clicks "share in viewer", the rev'd lhresults
is able to escape the online viewer's {{}}
, passes additional JSON.parse()
and DOM construction calls, then has code to automatically call createGist()
(we don't provide a way to call this; creating a gist is behind user gesture atm). The also needs to already have an oauth token.
If something were to ahppen, our oauth tokens are limited scoped. They can only create gists in the users account. They're also secret by default (for extra feel goods) and can be revoked by the user at any time.
The theme of this PR is to encourage users to share reports. Given the current low volume of sharers, let's land this so we can grow some us LH users!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The thing we're trying to prevent is a malicious actor creating gists in a user's account
Ideally we should prevent them from doing anything, of course. It's better that way but it also helps us down the line as we add new features, things get more complicated, etc. lhresults
is still used in the extension's report, and while it's stripped out in replaceReportHTML()
right now, it's possible we'll want to use it at some point in the future.
I'm not sure this does anything different other than adding an additional parse.
It is an additional parse, but it means the only cost is an additional parse. Meanwhile everything is escaped and no matter the user generated content it won't be able to break out. If we assign the object in js we're doing an eval with an optional innerHTML if it can close the element, with all the danger that entails. JS escaping is fairly complicated; this seems like an easy way to make sure we have it right (at least as a defense-in-depth thing with the rest of our efforts).
Handlebars, #1188, and #996 already force entity escaping everywhere that's needed.
The various other template entries are protected, but {{{lhresults}}}
is raw and only protected by escaping the closing </script>
tag from #996. Unless we're very careful it's possible someone will be able to do the double escape trick to get those back (or try one of the various other tricks in that doc) with our current system or in the future. Handlebars and the markdown change don't touch lhresults
since it's using the triple stash, and we're adding new user content all the time (e.g. the new CSS usage audit, which adds verbatim quotes from stylesheets in its extendedInfo
).
Given the current low volume of sharers, let's land this so we can grow some us LH users!
Totally agree. The main thing is that this is a fairly trivial change with no real overhead (semantically or performance-wise). The escaping that needs to happen is small in scope and already handled by Handlebars. It keeps us to the guarantee that the only things in {{{}}}
is content completely generated by LH itself.
It's really just
<div id="lhresults">{{lhresults}}</div>
{{#if scripts }}
<script>
// Use escaped results object to protect against XSS from result details.
window.lhresults = JSON.parse(document.querySelector('#lhresults').textContent);
</script>
{{/if}}
(with display: none
in the css) and everything else acts as it does now with no other changes.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Really not trying to be argumentative, this just seems like an easy way to secure us for the future :)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I also prefer putting the lhresults in a non-script element. How about <template>
?
TBH when i was first trying out the lhresults, it created some html elements from various descriptions. I think those things are addressed now, but i feel safer not throwing fairly arbitrary text into a script tag.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I kept the _escapeScriptTags
. Defense in depth-n-stuff.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nice!
📜
🔽
✉️ ➡️ ☁️
R: @paulirish @patrickhulce @brendankenny
This adds a button to the nav. When clicked, the local report opens the viewer ( http://googlechrome.github.io/lighthouse/viewer/) in a new tab and
postMessage
's the report json to it. The online viewer has amessage
listener that responds and constructs the page. This functionality will encourage folks to use the online viewer (where we have save as gist, shareable links, export functionality, etc.) It may also be a solution for the DevtTools integration.On http://googlechrome.github.io/lighthouse/viewer/, the "view online" button will be disabled:
On html reports generated by the CLI or the extension, the button will be enabled:
![screen shot 2016-12-19 at 12 56 06 pm](https://cloud.githubusercontent.com/assets/238208/21328849/a7277474-c5ea-11e6-84d1-2a66ac4bafe4.png)
Security concerns were addressed in #1188 by removing
{{{}}}
usage of user-provided content.