Skip to content

Commit

Permalink
[notifications] disallow arbitrary html in message content
Browse files Browse the repository at this point in the history
this way we can be sure that no scripts can be executed.

Signed-off-by: Alex Tugarev <[email protected]>
  • Loading branch information
AlexTugarev committed Mar 9, 2020
1 parent 842e717 commit a7ec808
Show file tree
Hide file tree
Showing 4 changed files with 96 additions and 19 deletions.
2 changes: 2 additions & 0 deletions packages/messages/src/browser/messages-frontend-module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,10 @@ import { NotificationsContribution, NotificationsKeybindingContext } from './not
import { FrontendApplicationContribution, KeybindingContribution, KeybindingContext } from '@theia/core/lib/browser';
import { CommandContribution } from '@theia/core';
import { ColorContribution } from '@theia/core/lib/browser/color-application-contribution';
import { NotificationContentRenderer } from './notification-content-renderer';

export default new ContainerModule((bind, unbind, isBound, rebind) => {
bind(NotificationContentRenderer).toSelf().inSingletonScope();
bind(NotificationsRenderer).toSelf().inSingletonScope();
bind(NotificationsContribution).toSelf().inSingletonScope();
bind(FrontendApplicationContribution).toService(NotificationsContribution);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
/********************************************************************************
* Copyright (C) 2020 TypeFox and others.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License v. 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0.
*
* This Source Code may also be made available under the following Secondary
* Licenses when the conditions for such availability set forth in the Eclipse
* Public License v. 2.0 are satisfied: GNU General Public License, version 2
* with the GNU Classpath Exception which is available at
* https://www.gnu.org/software/classpath/license.html.
*
* SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
********************************************************************************/

import { expect } from 'chai';
import { NotificationContentRenderer } from './notification-content-renderer';

/* eslint-disable no-unused-expressions */

describe('notification-content-renderer', () => {

const contentRnderer = new NotificationContentRenderer();

it('should remove new lines', () => {
expectRenderedContent('foo\nbar', 'foo bar');
expectRenderedContent('foo\n\n\nbar', 'foo bar');
});

it('should render links', () => {
expectRenderedContent(
'Link to [theia](https://github.com/eclipse-theia/theia)!',
'Link to <a href="https://github.com/eclipse-theia/theia">theia</a>!'
);
expectRenderedContent(
'Link to [theia](https://github.com/eclipse-theia/theia "title on hover")!',
'Link to <a href="https://github.com/eclipse-theia/theia" title="title on hover">theia</a>!'
);
expectRenderedContent(
'Click [here](command:my-command-id) to open stuff!',
'Click <a href="command:my-command-id">here</a> to open stuff!'
);
expectRenderedContent(
'Click [here](javascript:window.alert();) to open stuff!',
'Click [here](javascript:window.alert();) to open stuff!'
);
});

it('should render markdown', () => {
expectRenderedContent(
'*italic*',
'<em>italic</em>'
);
expectRenderedContent(
'**bold**',
'<strong>bold</strong>'
);
});

it('should not render html', () => {
expectRenderedContent(
'<script>document.getElementById("demo").innerHTML = "Hello JavaScript!";</script>',
'&lt;script&gt;document.getElementById(&quot;demo&quot;).innerHTML = &quot;Hello JavaScript!&quot;;&lt;/script&gt;'
);
expectRenderedContent(
'<a href="javascript:window.alert();">foobar</a>',
'&lt;a href=&quot;javascript:window.alert();&quot;&gt;foobar&lt;/a&gt;'
);
});

const expectRenderedContent = (input: string, output: string) =>
expect(contentRnderer.renderMessage(input)).to.be.equal(output);

});
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/********************************************************************************
* Copyright (C) 2017 TypeFox and others.
* Copyright (C) 2020 TypeFox and others.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License v. 2.0 which is available at
Expand All @@ -14,16 +14,18 @@
* SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
********************************************************************************/

/* note: this bogus test file is required so that
we are able to run mocha unit tests on this
package, without having any actual unit tests in it.
This way a coverage report will be generated,
showing 0% coverage, instead of no report.
This file can be removed once we have real unit
tests in place. */
import * as markdownit from 'markdown-it';
import { injectable } from 'inversify';

describe('messages package', () => {
@injectable()
export class NotificationContentRenderer {

it('support code coverage statistics', () => true);
protected readonly mdEngine = markdownit({ html: false });

});
renderMessage(content: string): string {
// in alignment with vscode, new lines aren't supported
const contentWithoutNewlines = content.replace(/((\r)?\n)+/gm, ' ');

return this.mdEngine.renderInline(contentWithoutNewlines);
}
}
14 changes: 6 additions & 8 deletions packages/messages/src/browser/notifications-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,12 @@ import { deepClone } from '@theia/core/lib/common/objects';
import { Emitter } from '@theia/core';
import { Deferred } from '@theia/core/lib/common/promise-util';
import { Md5 } from 'ts-md5';
import * as markdownit from 'markdown-it';
import throttle = require('lodash.throttle');
import { NotificationPreferences } from './notification-preferences';
import { ContextKeyService, ContextKey } from '@theia/core/lib/browser/context-key-service';
import { OpenerService } from '@theia/core/lib/browser';
import URI from '@theia/core/lib/common/uri';
import { NotificationContentRenderer } from './notification-content-renderer';

export interface NotificationUpdateEvent {
readonly notifications: Notification[];
Expand Down Expand Up @@ -60,6 +60,9 @@ export class NotificationManager extends MessageClient {
@inject(OpenerService)
protected readonly openerService: OpenerService;

@inject(NotificationContentRenderer)
protected readonly contentRenderer: NotificationContentRenderer;

protected readonly onUpdatedEmitter = new Emitter<NotificationUpdateEvent>();
readonly onUpdated = this.onUpdatedEmitter.event;
protected readonly fireUpdatedEvent = throttle(() => {
Expand Down Expand Up @@ -164,7 +167,7 @@ export class NotificationManager extends MessageClient {

let notification = this.notifications.get(messageId);
if (!notification) {
const message = this.renderMessage(plainMessage.text);
const message = this.contentRenderer.renderMessage(plainMessage.text);
const type = this.toNotificationType(plainMessage.type);
const actions = Array.from(new Set(plainMessage.actions));
const source = plainMessage.source;
Expand Down Expand Up @@ -208,11 +211,6 @@ export class NotificationManager extends MessageClient {
}
return plainMessage.options && plainMessage.options.timeout || this.preferences['notification.timeout'];
}
protected readonly mdEngine = markdownit({ html: true });
protected renderMessage(content: string): string {
const contentWithoutNewlines = content.replace(/(\r)?\n/gm, ' ');
return this.mdEngine.renderInline(contentWithoutNewlines);
}
protected isExpandable(message: string, source: string | undefined, actions: string[]): boolean {
if (!actions.length && source) {
return true;
Expand All @@ -238,7 +236,7 @@ export class NotificationManager extends MessageClient {
async showProgress(messageId: string, plainMessage: ProgressMessage, cancellationToken: CancellationToken): Promise<string | undefined> {
let notification = this.notifications.get(messageId);
if (!notification) {
const message = this.renderMessage(plainMessage.text);
const message = this.contentRenderer.renderMessage(plainMessage.text);
const type = this.toNotificationType(plainMessage.type);
const actions = Array.from(new Set(plainMessage.actions));
const source = plainMessage.source;
Expand Down

0 comments on commit a7ec808

Please sign in to comment.