mirror of https://github.com/calzoneman/sync.git
Enforce stricter validation on polls
This commit is contained in:
parent
41a538c655
commit
9dc82ad444
6
NEWS.md
6
NEWS.md
|
@ -1,3 +1,9 @@
|
||||||
|
2017-03-20
|
||||||
|
==========
|
||||||
|
|
||||||
|
Polls are now more strictly validated, including the number of options. The
|
||||||
|
default limit is 50 options, which you can configure via `poll.max-options`.
|
||||||
|
|
||||||
2017-03-11
|
2017-03-11
|
||||||
==========
|
==========
|
||||||
|
|
||||||
|
|
|
@ -250,3 +250,6 @@ service-socket:
|
||||||
# Twitch Client ID for the data API (used for VOD lookups)
|
# Twitch Client ID for the data API (used for VOD lookups)
|
||||||
# https://github.com/justintv/Twitch-API/blob/master/authentication.md#developer-setup
|
# https://github.com/justintv/Twitch-API/blob/master/authentication.md#developer-setup
|
||||||
twitch-client-id: null
|
twitch-client-id: null
|
||||||
|
|
||||||
|
poll:
|
||||||
|
max-options: 50
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
"author": "Calvin Montgomery",
|
"author": "Calvin Montgomery",
|
||||||
"name": "CyTube",
|
"name": "CyTube",
|
||||||
"description": "Online media synchronizer and chat",
|
"description": "Online media synchronizer and chat",
|
||||||
"version": "3.34.3",
|
"version": "3.34.4",
|
||||||
"repository": {
|
"repository": {
|
||||||
"url": "http://github.com/calzoneman/sync"
|
"url": "http://github.com/calzoneman/sync"
|
||||||
},
|
},
|
||||||
|
@ -54,7 +54,7 @@
|
||||||
"postinstall": "./postinstall.sh",
|
"postinstall": "./postinstall.sh",
|
||||||
"server-dev": "babel -D --watch --source-maps --loose es6.destructuring,es6.forOf --out-dir lib/ src/",
|
"server-dev": "babel -D --watch --source-maps --loose es6.destructuring,es6.forOf --out-dir lib/ src/",
|
||||||
"generate-userscript": "$npm_node_execpath gdrive-userscript/generate-userscript $@ > www/js/cytube-google-drive.user.js",
|
"generate-userscript": "$npm_node_execpath gdrive-userscript/generate-userscript $@ > www/js/cytube-google-drive.user.js",
|
||||||
"test": "mocha",
|
"test": "mocha --recursive test",
|
||||||
"integration-test": "mocha --recursive integration_test"
|
"integration-test": "mocha --recursive integration_test"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
var ChannelModule = require("./module");
|
var ChannelModule = require("./module");
|
||||||
var Poll = require("../poll").Poll;
|
var Poll = require("../poll").Poll;
|
||||||
|
import { ValidationError } from '../errors';
|
||||||
|
import Config from '../config';
|
||||||
|
|
||||||
const TYPE_NEW_POLL = {
|
const TYPE_NEW_POLL = {
|
||||||
title: "string",
|
title: "string",
|
||||||
|
@ -130,16 +132,55 @@ PollModule.prototype.broadcastPoll = function (isNewPoll) {
|
||||||
this.channel.broadcastToRoom(event, obscured, this.roomNoViewHidden);
|
this.channel.broadcastToRoom(event, obscured, this.roomNoViewHidden);
|
||||||
};
|
};
|
||||||
|
|
||||||
PollModule.prototype.handleNewPoll = function (user, data) {
|
PollModule.prototype.validatePollInput = function validatePollInput(title, options) {
|
||||||
|
if (typeof title !== 'string') {
|
||||||
|
throw new ValidationError('Poll title must be a string.');
|
||||||
|
}
|
||||||
|
if (title.length > 255) {
|
||||||
|
throw new ValidationError('Poll title must be no more than 255 characters long.');
|
||||||
|
}
|
||||||
|
if (!Array.isArray(options)) {
|
||||||
|
throw new ValidationError('Poll options must be an array.');
|
||||||
|
}
|
||||||
|
if (options.length > Config.get('poll.max-options')) {
|
||||||
|
throw new ValidationError(`Polls are limited to a maximum of ${Config.get('poll.max-options')} options.`);
|
||||||
|
}
|
||||||
|
for (let i = 0; i < options.length; i++) {
|
||||||
|
if (typeof options[i] !== 'string') {
|
||||||
|
throw new ValidationError('Poll options must be strings.');
|
||||||
|
}
|
||||||
|
if (options[i].length === 0 || options[i].length > 255) {
|
||||||
|
throw new ValidationError('Poll options must be 1-255 characters long.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
PollModule.prototype.handleNewPoll = function (user, data, ack) {
|
||||||
if (!this.channel.modules.permissions.canControlPoll(user)) {
|
if (!this.channel.modules.permissions.canControlPoll(user)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var title = data.title.substring(0, 255);
|
if (typeof data !== 'object' || data === null) {
|
||||||
var opts = data.opts.map(function (x) { return (""+x).substring(0, 255); });
|
ack({
|
||||||
var obscured = data.obscured;
|
error: {
|
||||||
|
message: 'Invalid data received for poll creation.'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
var poll = new Poll(user.getName(), title, opts, obscured);
|
try {
|
||||||
|
this.validatePollInput(data.title, data.opts);
|
||||||
|
} catch (error) {
|
||||||
|
ack({
|
||||||
|
error: {
|
||||||
|
message: error.message
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var poll = new Poll(user.getName(), data.title, data.opts, data.obscured);
|
||||||
var self = this;
|
var self = this;
|
||||||
if (data.hasOwnProperty("timeout") && !isNaN(data.timeout) && data.timeout > 0) {
|
if (data.hasOwnProperty("timeout") && !isNaN(data.timeout) && data.timeout > 0) {
|
||||||
poll.timer = setTimeout(function () {
|
poll.timer = setTimeout(function () {
|
||||||
|
@ -155,6 +196,7 @@ PollModule.prototype.handleNewPoll = function (user, data) {
|
||||||
this.poll = poll;
|
this.poll = poll;
|
||||||
this.broadcastPoll(true);
|
this.broadcastPoll(true);
|
||||||
this.channel.logger.log("[poll] " + user.getName() + " opened poll: '" + poll.title + "'");
|
this.channel.logger.log("[poll] " + user.getName() + " opened poll: '" + poll.title + "'");
|
||||||
|
ack({});
|
||||||
};
|
};
|
||||||
|
|
||||||
PollModule.prototype.handleVote = function (user, data) {
|
PollModule.prototype.handleVote = function (user, data) {
|
||||||
|
@ -198,6 +240,16 @@ PollModule.prototype.handlePollCmd = function (obscured, user, msg, meta) {
|
||||||
|
|
||||||
var args = msg.split(",");
|
var args = msg.split(",");
|
||||||
var title = args.shift();
|
var title = args.shift();
|
||||||
|
|
||||||
|
try {
|
||||||
|
this.validatePollInput(title, args);
|
||||||
|
} catch (error) {
|
||||||
|
user.socket.emit('errorMsg', {
|
||||||
|
msg: 'Error creating poll: ' + error.message
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
var poll = new Poll(user.getName(), title, args, obscured);
|
var poll = new Poll(user.getName(), title, args, obscured);
|
||||||
this.poll = poll;
|
this.poll = poll;
|
||||||
this.broadcastPoll(true);
|
this.broadcastPoll(true);
|
||||||
|
|
|
@ -119,7 +119,10 @@ var defaults = {
|
||||||
"google-drive": {
|
"google-drive": {
|
||||||
"html5-hack-enabled": false
|
"html5-hack-enabled": false
|
||||||
},
|
},
|
||||||
"twitch-client-id": null
|
"twitch-client-id": null,
|
||||||
|
poll: {
|
||||||
|
"max-options": 50
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -7,3 +7,4 @@ export const CSRFError = createError('CSRFError');
|
||||||
export const HTTPError = createError('HTTPError', {
|
export const HTTPError = createError('HTTPError', {
|
||||||
status: HTTPStatus.INTERNAL_SERVER_ERROR
|
status: HTTPStatus.INTERNAL_SERVER_ERROR
|
||||||
});
|
});
|
||||||
|
export const ValidationError = createError('ValidationError');
|
|
@ -133,14 +133,14 @@ function ipLimitReached(sock) {
|
||||||
|
|
||||||
function addTypecheckedFunctions(sock) {
|
function addTypecheckedFunctions(sock) {
|
||||||
sock.typecheckedOn = function (msg, template, cb) {
|
sock.typecheckedOn = function (msg, template, cb) {
|
||||||
sock.on(msg, function (data) {
|
sock.on(msg, function (data, ack) {
|
||||||
typecheck(data, template, function (err, data) {
|
typecheck(data, template, function (err, data) {
|
||||||
if (err) {
|
if (err) {
|
||||||
sock.emit("errorMsg", {
|
sock.emit("errorMsg", {
|
||||||
msg: "Unexpected error for message " + msg + ": " + err.message
|
msg: "Unexpected error for message " + msg + ": " + err.message
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
cb(data);
|
cb(data, ack);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -0,0 +1,145 @@
|
||||||
|
const PollModule = require('../../lib/channel/poll');
|
||||||
|
const assert = require('assert');
|
||||||
|
const Config = require('../../lib/config');
|
||||||
|
|
||||||
|
describe('PollModule', () => {
|
||||||
|
describe('#validatePollInput', () => {
|
||||||
|
let pollModule = new PollModule({ uniqueName: 'testChannel', modules: {} });
|
||||||
|
|
||||||
|
it('accepts valid input', () => {
|
||||||
|
let title = '';
|
||||||
|
for (let i = 0; i < 20; i++) {
|
||||||
|
title += 'x';
|
||||||
|
}
|
||||||
|
|
||||||
|
pollModule.validatePollInput(title, ['ab', 'cd']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('rejects non-string titles', () => {
|
||||||
|
assert.throws(() => {
|
||||||
|
pollModule.validatePollInput(null, []);
|
||||||
|
}, /title/);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('rejects invalidly long titles', () => {
|
||||||
|
let title = '';
|
||||||
|
for (let i = 0; i < 256; i++) {
|
||||||
|
title += 'x';
|
||||||
|
}
|
||||||
|
|
||||||
|
assert.throws(() => {
|
||||||
|
pollModule.validatePollInput(title, []);
|
||||||
|
}, /title/);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('rejects non-array option parameter', () => {
|
||||||
|
assert.throws(() => {
|
||||||
|
pollModule.validatePollInput('poll', 1234);
|
||||||
|
}, /options/);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('rejects too many options', () => {
|
||||||
|
const limit = Config.get('poll.max-options');
|
||||||
|
Config.set('poll.max-options', 2);
|
||||||
|
try {
|
||||||
|
assert.throws(() => {
|
||||||
|
pollModule.validatePollInput('poll', ['1', '2', '3', '4']);
|
||||||
|
}, /maximum of 2 options/);
|
||||||
|
} finally {
|
||||||
|
Config.set('poll.max-options', limit);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('rejects non-string options', () => {
|
||||||
|
assert.throws(() => {
|
||||||
|
pollModule.validatePollInput('poll', [null]);
|
||||||
|
}, /options must be strings/);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('rejects invalidly long options', () => {
|
||||||
|
let option = '';
|
||||||
|
for (let i = 0; i < 256; i++) {
|
||||||
|
option += 'x';
|
||||||
|
}
|
||||||
|
|
||||||
|
assert.throws(() => {
|
||||||
|
pollModule.validatePollInput('poll', [option]);
|
||||||
|
}, /options must be 1-255 characters/);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('#handleNewPoll', () => {
|
||||||
|
let fakeChannel = {
|
||||||
|
uniqueName: 'testChannel',
|
||||||
|
logger: {
|
||||||
|
log() {
|
||||||
|
|
||||||
|
}
|
||||||
|
},
|
||||||
|
broadcastToRoom() {
|
||||||
|
},
|
||||||
|
broadcastAll() {
|
||||||
|
},
|
||||||
|
modules: {
|
||||||
|
permissions: {
|
||||||
|
canControlPoll() {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let fakeUser = {
|
||||||
|
getName() {
|
||||||
|
return 'testUser';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let pollModule = new PollModule(fakeChannel);
|
||||||
|
|
||||||
|
it('creates a valid poll', () => {
|
||||||
|
let sentNewPoll = false;
|
||||||
|
let sentClosePoll = false;
|
||||||
|
fakeChannel.broadcastToRoom = (event, data, room) => {
|
||||||
|
if (room === 'testChannel:viewHidden' && event === 'newPoll') {
|
||||||
|
sentNewPoll = true;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
fakeChannel.broadcastAll = (event) => {
|
||||||
|
if (event === 'closePoll') {
|
||||||
|
sentClosePoll = true;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
pollModule.handleNewPoll(fakeUser, {
|
||||||
|
title: 'test poll',
|
||||||
|
opts: [
|
||||||
|
'option 1',
|
||||||
|
'option 2'
|
||||||
|
],
|
||||||
|
obscured: false
|
||||||
|
}, (ackResult) => {
|
||||||
|
assert(!ackResult.error, `Unexpected error: ${ackResult.error}`);
|
||||||
|
});
|
||||||
|
assert(sentClosePoll, 'Expected broadcast of closePoll event');
|
||||||
|
assert(sentNewPoll, 'Expected broadcast of newPoll event');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('rejects an invalid poll', () => {
|
||||||
|
fakeChannel.broadcastToRoom = (event, data, room) => {
|
||||||
|
assert(false, 'Expected no events to be sent');
|
||||||
|
};
|
||||||
|
fakeChannel.broadcastAll = (event) => {
|
||||||
|
assert(false, 'Expected no events to be sent');
|
||||||
|
};
|
||||||
|
const options = [];
|
||||||
|
for (let i = 0; i < 200; i++) {
|
||||||
|
options.push('option ' + i);
|
||||||
|
}
|
||||||
|
pollModule.handleNewPoll(fakeUser, {
|
||||||
|
title: 'test poll',
|
||||||
|
opts: options,
|
||||||
|
obscured: false
|
||||||
|
}, (ackResult) => {
|
||||||
|
assert.equal(ackResult.error.message, 'Polls are limited to a maximum of 50 options.');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
})
|
||||||
|
});
|
|
@ -789,6 +789,7 @@ function showPollMenu() {
|
||||||
$("<strong/>").text("Title").appendTo(menu);
|
$("<strong/>").text("Title").appendTo(menu);
|
||||||
|
|
||||||
var title = $("<input/>").addClass("form-control")
|
var title = $("<input/>").addClass("form-control")
|
||||||
|
.attr("maxlength", "255")
|
||||||
.attr("type", "text")
|
.attr("type", "text")
|
||||||
.appendTo(menu);
|
.appendTo(menu);
|
||||||
|
|
||||||
|
@ -820,6 +821,7 @@ function showPollMenu() {
|
||||||
function addOption() {
|
function addOption() {
|
||||||
$("<input/>").addClass("form-control")
|
$("<input/>").addClass("form-control")
|
||||||
.attr("type", "text")
|
.attr("type", "text")
|
||||||
|
.attr("maxlength", "255")
|
||||||
.addClass("poll-menu-option")
|
.addClass("poll-menu-option")
|
||||||
.insertBefore(addbtn);
|
.insertBefore(addbtn);
|
||||||
}
|
}
|
||||||
|
@ -859,8 +861,16 @@ function showPollMenu() {
|
||||||
opts: opts,
|
opts: opts,
|
||||||
obscured: hidden.prop("checked"),
|
obscured: hidden.prop("checked"),
|
||||||
timeout: t
|
timeout: t
|
||||||
|
}, function ack(result) {
|
||||||
|
if (result.error) {
|
||||||
|
modalAlert({
|
||||||
|
title: 'Error creating poll',
|
||||||
|
textContent: result.error.message
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
menu.remove();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
menu.remove();
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue