fix for possible xss exploits
This commit is contained in:
+36
-1
@@ -66,7 +66,7 @@ export function arrayEqual(a, b) {
|
|||||||
// as close to E.164 as possible.
|
// as close to E.164 as possible.
|
||||||
export function asPhone(val) {
|
export function asPhone(val) {
|
||||||
val = val.trim();
|
val = val.trim();
|
||||||
if (/^\s*(?:\+?(\d{1,3}))?[- (.]*(\d{3})[- ).]*(\d{3})[- .]*(\d{2})[- .]*(\d{2})?\s*$/.test(val)) {
|
if (/^(?:\+?(\d{1,3}))?[- (.]*(\d{3})[- ).]*(\d{3})[- .]*(\d{2})[- .]*(\d{2})?$/.test(val)) {
|
||||||
return val.replace(/[- ().]*/, '');
|
return val.replace(/[- ().]*/, '');
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
@@ -80,3 +80,38 @@ export function asEmail(val) {
|
|||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Checks if URL is a relative url, i.e. has no 'scheme://', including the case of missing scheme '//'.
|
||||||
|
// The scheme is expected to be RFC-compliant, e.g. [a-z][a-z0-9+.-]*
|
||||||
|
// example.html - ok
|
||||||
|
// https:example.com - not ok.
|
||||||
|
// http:/example.com - not ok.
|
||||||
|
// ' ↲ https://example.com' - not ok. (↲ means carriage return)
|
||||||
|
export function isUrlRelative(url) {
|
||||||
|
return !/^\s*([a-z][a-z0-9+.-]*:|\/\/)/im.test(url);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure URL is valid and does not present an XSS risk. Optional allowedSchemes may contain an array of
|
||||||
|
// strings with permitted URL schemes, such as ['ftp', 'ftps']; otherwise accept http and https only.
|
||||||
|
export function sanitizeUrl(url, allowedSchemes) {
|
||||||
|
if (!url) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Strip control characters and whitespace. They are not valid URL characters anyway.
|
||||||
|
url = url.replace(/[^\x20-\x7E]/gmi, '').trim();
|
||||||
|
|
||||||
|
// Relative URLs are safe.
|
||||||
|
if (!/^([a-z][a-z0-9+.-]*:|\/\/)/i.test(url)) {
|
||||||
|
return url;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Absolute URL. Accept only safe schemes, or no scheme.
|
||||||
|
const schemes = Array.isArray(allowedSchemes) ? allowedSchemes.join('|') : 'http|https';
|
||||||
|
const re = new RegExp('^((' + schemes + '):|//)', 'i');
|
||||||
|
if (!re.test(url)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return url;
|
||||||
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { FormattedMessage } from 'react-intl';
|
|||||||
|
|
||||||
import FileProgress from './file-progress.jsx';
|
import FileProgress from './file-progress.jsx';
|
||||||
import { bytesToHumanSize } from '../lib/strformat.js';
|
import { bytesToHumanSize } from '../lib/strformat.js';
|
||||||
|
import { isUrlRelative, sanitizeUrl } from '../lib/utils.js';
|
||||||
|
|
||||||
export default class Attachment extends React.Component {
|
export default class Attachment extends React.Component {
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
@@ -53,8 +54,7 @@ export default class Attachment extends React.Component {
|
|||||||
// If the URL is relative use LargeFileHelper to attach authentication
|
// If the URL is relative use LargeFileHelper to attach authentication
|
||||||
// credentials to the request.
|
// credentials to the request.
|
||||||
let url, helperFunc;
|
let url, helperFunc;
|
||||||
if (!this.props.uploader && !this.state.downloader &&
|
if (!this.props.uploader && !this.state.downloader && isUrlRelative(this.props.downloadUrl)) {
|
||||||
!(/^(?:(?:[a-z]+:)?\/\/)/i.test(this.props.downloadUrl))) {
|
|
||||||
// Relative URL. Use download helper.
|
// Relative URL. Use download helper.
|
||||||
url = '#';
|
url = '#';
|
||||||
helperFunc = (e) => {
|
helperFunc = (e) => {
|
||||||
@@ -62,7 +62,7 @@ export default class Attachment extends React.Component {
|
|||||||
this.downloadFile(this.props.downloadUrl, this.props.filename, this.props.mimetype);
|
this.downloadFile(this.props.downloadUrl, this.props.filename, this.props.mimetype);
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
url = this.props.downloadUrl;
|
url = sanitizeUrl(this.props.downloadUrl) || 'about:blank';
|
||||||
helperFunc = null;
|
helperFunc = null;
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { Drafty } from 'tinode-sdk'
|
|||||||
import Attachment from './attachment.jsx';
|
import Attachment from './attachment.jsx';
|
||||||
import LetterTile from './letter-tile.jsx';
|
import LetterTile from './letter-tile.jsx';
|
||||||
import ReceivedMarker from './received-marker.jsx'
|
import ReceivedMarker from './received-marker.jsx'
|
||||||
|
import { sanitizeUrl } from '../lib/utils.js';
|
||||||
|
|
||||||
export default class ChatMessage extends React.Component {
|
export default class ChatMessage extends React.Component {
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
@@ -48,7 +49,7 @@ export default class ChatMessage extends React.Component {
|
|||||||
e.target.dataset.val === undefined ? 1 : '' + e.target.dataset.val;
|
e.target.dataset.val === undefined ? 1 : '' + e.target.dataset.val;
|
||||||
}
|
}
|
||||||
if (e.target.dataset.act == 'url') {
|
if (e.target.dataset.act == 'url') {
|
||||||
data.ref = '' + e.target.dataset.ref;
|
data.ref = sanitizeUrl(e.target.dataset.ref) || 'about:blank';
|
||||||
}
|
}
|
||||||
const text = e.target.dataset.title || 'unknown';
|
const text = e.target.dataset.title || 'unknown';
|
||||||
this.props.onFormResponse(e.target.dataset.act, text, data);
|
this.props.onFormResponse(e.target.dataset.act, text, data);
|
||||||
@@ -163,6 +164,7 @@ function draftyFormatter(style, data, values, key) {
|
|||||||
Math.min(this.props.viewportWidth - REM_SIZE * 4, REM_SIZE * 36), REM_SIZE * 24, false);
|
Math.min(this.props.viewportWidth - REM_SIZE * 4, REM_SIZE * 36), REM_SIZE * 24, false);
|
||||||
dim = dim || {dstWidth: BROKEN_IMAGE_SIZE, dstHeight: BROKEN_IMAGE_SIZE};
|
dim = dim || {dstWidth: BROKEN_IMAGE_SIZE, dstHeight: BROKEN_IMAGE_SIZE};
|
||||||
attr.style = { width: dim.dstWidth + 'px', height: dim.dstHeight + 'px' };
|
attr.style = { width: dim.dstWidth + 'px', height: dim.dstHeight + 'px' };
|
||||||
|
attr.src = sanitizeUrl(attr.src);
|
||||||
if (attr.src) {
|
if (attr.src) {
|
||||||
attr.onClick = this.handlePreviewImage;
|
attr.onClick = this.handlePreviewImage;
|
||||||
attr.className += ' image-clickable';
|
attr.className += ' image-clickable';
|
||||||
|
|||||||
+41
-11
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+1
-1
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user