FIXED MASSIVE SECURITY ISSUE! oh and eslint

This commit is contained in:
Evert Prants 2021-12-15 19:31:02 +02:00
parent 07a05621e7
commit 7ec9cead2a
Signed by: evert
GPG Key ID: 1688DA83D222D0B5
12 changed files with 2751 additions and 321 deletions

1
.eslintignore Normal file
View File

@ -0,0 +1 @@
*.js

47
.eslintrc.js Normal file
View File

@ -0,0 +1,47 @@
module.exports = {
'env': {
'es2021': true,
'node': true
},
'extends': [
'eslint:recommended',
'plugin:@typescript-eslint/recommended'
],
'parser': '@typescript-eslint/parser',
'parserOptions': {
'ecmaVersion': 13,
'sourceType': 'module',
'project': 'tsconfig.json',
'tsconfigRootDir': __dirname,
},
'plugins': [
'@typescript-eslint'
],
'rules': {
'no-empty': [
'error',
{
'allowEmptyCatch': true,
}
],
'@typescript-eslint/no-explicit-any': 'off',
'@typescript-eslint/no-unused-vars': 'off',
'indent': [
'error',
2
],
'linebreak-style': [
'error',
'unix'
],
'quotes': [
'error',
'single'
],
'semi': [
'error',
'always'
]
}
};

3
.gitignore vendored
View File

@ -2,5 +2,6 @@
/.out/
deployment.json
*.js
!.eslintrc.js
*.d.ts
*.tsbuildinfo
*.tsbuildinfo

View File

@ -50,6 +50,7 @@ export class IRCFormatter extends Formatter {
}
public strip(msg: string): string {
// eslint-disable-next-line no-control-regex
return msg.replace(/(\x03\d{0,2}(,\d{0,2})?)/g, '').replace(/[\x0F\x02\x16\x1F]/g, '');
}
@ -72,22 +73,22 @@ export class IRCFormatter extends Formatter {
// Special types
if (elemParams && elemParams.type) {
switch (elemParams.type) {
case 'time':
elemValue = new Date(elemValue).toString();
break;
case 'metric':
elemValue = thousandsSeparator(elemValue);
break;
case 'timesince':
elemValue = timeSince(elemValue);
break;
case 'duration':
elemValue = toHHMMSS(elemValue);
break;
case 'description':
valueColor = 'blue';
elemValue = `"${elemValue}"`;
break;
case 'time':
elemValue = new Date(elemValue).toString();
break;
case 'metric':
elemValue = thousandsSeparator(elemValue);
break;
case 'timesince':
elemValue = timeSince(elemValue);
break;
case 'duration':
elemValue = toHHMMSS(elemValue);
break;
case 'description':
valueColor = 'blue';
elemValue = `"${elemValue}"`;
break;
}
}

View File

@ -25,20 +25,31 @@ export interface IIRCMessage {
raw: IIRCLine;
}
export interface IQueue {
await: string;
from: string;
do: (line: IIRCLine) => void;
}
export interface INickStore {
checked: number;
result: boolean;
}
declare type ConnectSocket = TLSSocket | Socket;
export class IRC extends EventEmitter {
public alive = false;
private authenticated = false;
private serverData: {[key: string]: any} = {
public authenticated = false;
public serverData: { [key: string]: any } = {
name: '',
supportedModes: {},
serverSupports: {},
};
private queue: any[] = [];
private channels: string[] = [];
private nickservStore: {[key: string]: any} = {};
public queue: IQueue[] = [];
public channels: string[] = [];
public nickservStore: { [key: string]: INickStore } = {};
private socket: ConnectSocket | null = null;
@ -83,7 +94,7 @@ export class IRC extends EventEmitter {
this.joinMissingChannels(this.options.channels);
});
this.on('testnick', (data) => {
this.on('testnick', (data: { nickname: string; func: (result: boolean) => void }) => {
if (this.nickservStore[data.nickname] != null) {
if (this.nickservStore[data.nickname].result === true) {
data.func(true);
@ -95,25 +106,28 @@ export class IRC extends EventEmitter {
}
}
if (this.options.nickserv && this.options.nickserv.enabled && this.options.nickserv.command) {
if (this.options.nickserv
&& this.options.nickserv.enabled
&& this.options.nickserv.command
) {
this.queue.push({
await: 'NOTICE',
from: 'NickServ',
do: (line: IIRCLine) => {
const splitline = line.trailing!.split(' ');
if (splitline![1] !== '0') {
this.nickservStore[data.nickname] = {
result: true,
checked: Date.now(),
};
data.func(true);
} else {
this.nickservStore[data.nickname] = {
result: false,
checked: Date.now(),
};
data.func(false);
if (!line.trailing) {
return data.func(false);
}
const splitline = line.trailing.trim().split(' ');
let result = false;
if (splitline[2] !== '0') {
result = true;
}
this.nickservStore[data.nickname] = {
result, checked: Date.now(),
};
data.func(result);
}
});
this.write('PRIVMSG nickserv :%s %s', this.options.nickserv.command, data.nickname);
@ -134,7 +148,7 @@ export class IRC extends EventEmitter {
if (!this.alive) {
return;
}
this.socket!.write(data + '\r\n');
this.socket?.write(data + '\r\n');
}
private joinMissingChannels(arr: string[]): void {
@ -155,158 +169,169 @@ export class IRC extends EventEmitter {
private handleServerLine(line: IIRCLine): void {
if (this.queue.length) {
let skipHandling = false;
for (const i in this.queue) {
const entry = this.queue[i];
const afterModifyQueue: IQueue[] = [];
this.queue.forEach((entry) => {
if (entry.await && line.command === entry.await) {
if (entry.from && line.user.nickname.toLowerCase() === entry.from.toLowerCase()) {
if (entry.do) {
skipHandling = true;
this.queue.splice(parseInt(i, 10), 1);
entry.do(line);
return;
}
}
}
}
afterModifyQueue.push(entry);
});
this.queue = afterModifyQueue;
if (skipHandling) {
return;
}
}
switch (line.command.toLowerCase()) {
case 'cap':
if (line.trailing === 'sasl' && line.arguments![1] === 'ACK' && !this.authenticated) {
this.write('AUTHENTICATE PLAIN');
}
break;
case '+':
case ':+':
if (this.authenticated) {
return;
}
case 'cap':
if (line.trailing === 'sasl' && line.arguments?.[1] === 'ACK' && !this.authenticated) {
this.write('AUTHENTICATE PLAIN');
}
break;
case '+':
case ':+': {
if (this.authenticated) {
return;
}
const authline = Buffer.from(this.options.nick + '\x00' + this.options.username + '\x00' + this.options.password)
.toString('base64');
this.write('AUTHENTICATE %s', authline);
break;
case '904':
this.emit('error', {
error: new Error(line.trailing),
fatal: true
});
break;
case '903':
this.write('CAP END');
break;
case 'notice':
case 'privmsg':
if (!line.user.nickname || line.user.nickname === '') {
return;
}
this.emit('message', {
message: line.trailing,
to: line.arguments![0],
nickname: line.user.nickname,
raw: line
});
break;
case '001':
this.serverData.name = line.user.hostname;
this.authenticated = true;
const authline = Buffer.from(this.options.nick + '\x00' + this.options.username + '\x00' + this.options.password)
.toString('base64');
this.write('AUTHENTICATE %s', authline);
break;
}
case '904':
this.emit('error', {
error: new Error(line.trailing),
fatal: true
});
break;
case '903':
this.write('CAP END');
break;
case 'notice':
case 'privmsg':
if (!line.user.nickname || line.user.nickname === '') {
return;
}
this.emit('message', {
message: line.trailing,
to: line.arguments?.[0],
nickname: line.user.nickname,
raw: line
});
break;
case '001':
this.serverData.name = line.user.hostname;
this.authenticated = true;
// Set nick to what the server actually thinks is our nick
this.options.nick = line.arguments![0];
this.emit('authenticated', true);
// Set nick to what the server actually thinks is our nick
this.options.nick = line.arguments?.[0] || 'Squeebot';
this.emit('authenticated', true);
// Send a whois request for self in order to reliably fetch hostname of self
this.write('WHOIS %s', this.options.nick);
// Send a whois request for self in order to reliably fetch hostname of self
this.write('WHOIS %s', this.options.nick);
break;
case '005': {
if (!line.arguments) {
break;
case '005':
const argv = line.arguments!.slice(1);
for (const a in argv) {
let t: any = argv[a];
if (t.indexOf('=') !== -1) {
t = t.split('=');
if (t[0] === 'PREFIX') {
const d = t[1].match(/\((\w+)\)(.*)/);
const r = d![1].split('');
const aa = d![2].split('');
for (const b in r) {
this.serverData.supportedModes[r[b]] = aa[b];
}
} else if (t[0] === 'NETWORK') {
this.serverData.network = t[1];
} else if (t[0] === 'CHANNELLEN') {
this.serverData.maxChannelLength = parseInt(t[1], 10);
}
const argv = line.arguments?.slice(1);
for (const entry of argv) {
if (entry.indexOf('=') !== -1) {
const t = entry.split('=') as string[];
if (t[0] === 'PREFIX') {
const d = t[1].match(/\((\w+)\)(.*)/);
if (d) {
const r = d[1].split('');
const aa = d[2].split('');
r.forEach((value, index) => {
this.serverData.supportedModes[value] = aa[index];
});
}
if (!isNaN(parseInt(t[1], 10))) {
t[1] = parseInt(t[1], 10);
}
this.serverData.serverSupports[t[0]] = t[1];
} else {
this.serverData.serverSupports[t] = true;
} else if (t[0] === 'NETWORK') {
this.serverData.network = t[1];
} else if (t[0] === 'CHANNELLEN') {
this.serverData.maxChannelLength = parseInt(t[1], 10);
}
let numeral: string | number = t[1];
if (!isNaN(parseInt(numeral, 10))) {
numeral = parseInt(numeral, 10);
}
this.serverData.serverSupports[t[0]] = numeral;
} else {
this.serverData.serverSupports[entry] = true;
}
break;
// Set hostname from 396 (non-standard)
case '396':
this.options.hostname = line.arguments![1];
break;
}
break;
}
// Set hostname from 396 (non-standard)
case '396':
this.options.hostname = line.arguments?.[1];
break;
// Set hostname from self-whois
case '311':
if (line.arguments![1] !== this.options.nick) {
return;
}
this.options.hostname = line.arguments![3];
break;
case 'quit':
if (line.user.nickname !== this.options.nick) {
if (this.nickservStore[line.user.nickname]) {
delete this.nickservStore[line.user.nickname];
}
this.emit('leave', {
nickname: line.user.nickname
});
}
break;
case 'nick':
if (line.user.nickname === this.options.nick) {
this.options.nick = line.arguments![0];
} else if (this.nickservStore[line.user.nickname]) {
case '311':
if (line.arguments?.[1] !== this.options.nick) {
return;
}
this.options.hostname = line.arguments?.[3];
break;
case 'quit':
if (line.user.nickname !== this.options.nick) {
if (this.nickservStore[line.user.nickname]) {
delete this.nickservStore[line.user.nickname];
}
this.emit('nick', {
oldNick: line.user.nickname,
newNick: line.arguments![0]
});
break;
case 'join':
if (line.user.nickname === this.options.nick && line.trailing) {
this.channels.push(line.trailing);
}
this.emit('join', {
nickname: line.user.nickname,
channel: line.trailing
});
break;
case 'part':
case 'kick':
if (line.user.nickname === this.options.nick) {
const indexAt = this.channels.indexOf(line.arguments![0]);
if (indexAt !== -1) {
this.channels.splice(indexAt, 1);
}
}
this.emit('leave', {
nickname: line.user.nickname,
channel: line.arguments![0]
nickname: line.user.nickname
});
break;
case 'error':
this.emit('error', { fatal: true, error: new Error(line.raw) });
break;
}
break;
case 'nick':
if (line.user.nickname === this.options.nick) {
this.options.nick = line.arguments?.[0] || 'unknown';
} else if (this.nickservStore[line.user.nickname]) {
delete this.nickservStore[line.user.nickname];
}
this.emit('nick', {
oldNick: line.user.nickname,
newNick: line.arguments?.[0]
});
break;
case 'join':
if (line.user.nickname === this.options.nick && line.trailing) {
this.channels.push(line.trailing);
}
this.emit('join', {
nickname: line.user.nickname,
channel: line.trailing
});
break;
case 'part':
case 'kick':
if (line.user.nickname === this.options.nick && line.arguments) {
const indexAt = this.channels.indexOf(line.arguments[0]);
if (indexAt !== -1) {
this.channels.splice(indexAt, 1);
}
}
this.emit('leave', {
nickname: line.user.nickname,
channel: line.arguments?.[0]
});
break;
case 'error':
this.emit('error', { fatal: true, error: new Error(line.raw) });
break;
}
}
@ -321,8 +346,8 @@ export class IRC extends EventEmitter {
// Predict the length the server is going to split at
// :nickname!username@hostname command args :trailing\r\n
const header = this.options.nick.length +
this.options.hostname!.length +
this.options.username!.length + 4 + 2;
(this.options.hostname || '').length +
(this.options.username || '').length + 4 + 2;
const offset = command.length + args.length + 3 + header;
// Split the message up into chunks
@ -383,14 +408,14 @@ export class IRC extends EventEmitter {
this.socket = connection;
let buffer: any = '';
this.socket!.on('data', (chunk) => {
this.socket?.on('data', (chunk) => {
buffer += chunk;
const data = buffer.split('\r\n');
buffer = data.pop();
data.forEach((line: string) => {
if (line.indexOf('PING') === 0) {
this.socket!.write('PONG' + line.substring(4) + '\r\n');
this.socket?.write('PONG' + line.substring(4) + '\r\n');
return;
}

View File

@ -3,7 +3,7 @@
"name": "irc",
"description": "IRC Service for Squeebot 3",
"tags": ["service", "irc"],
"version": "1.1.3",
"version": "1.2.0",
"dependencies": ["control?"],
"npmDependencies": []
}

View File

@ -14,6 +14,7 @@ import { logger } from '@squeebot/core/lib/core';
import { IIRCMessage, IRC } from './irc';
import { IRCFormatter } from './format';
import { IIRCLine } from './parser';
class IRCMessage implements IMessage {
public time: Date = new Date();
@ -95,26 +96,28 @@ class IRCProtocol extends Protocol {
this.emit('stopped');
}
private validateNick(nickname: string, cb: Function): void {
if (!this.config.nickserv || !this.config.nickserv.enabled) {
return cb(true); // Assume the user is authentic
private async validateNick(nickname: string): Promise<boolean> {
if (!this.config.irc.nickserv || !this.config.irc.nickserv.enabled) {
return true; // Assume the user is authentic
}
let stop = false;
const promiseTimeout = setTimeout(() => {
stop = true;
cb(false);
}, 4000);
return new Promise((resolve) => {
let stop = false;
const promiseTimeout = setTimeout(() => {
stop = true;
resolve(false);
}, 4000);
this.irc.emit('testnick', {
nickname,
func: (result: boolean) => {
clearTimeout(promiseTimeout);
if (stop) {
return;
this.irc.emit('testnick', {
nickname,
func: (result: boolean) => {
clearTimeout(promiseTimeout);
if (stop) {
return;
}
resolve(result);
}
cb(result);
}
});
});
}
@ -175,7 +178,7 @@ class IRCProtocol extends Protocol {
});
this.irc.on('message', (msg: IIRCMessage) => {
this.validateNick(msg.nickname, (valid: boolean) => {
this.validateNick(msg.nickname).then((valid: boolean) => {
const to = msg.to === this.irc.options.nick ? msg.nickname : msg.to;
const newMessage = new IRCMessage(
EMessageType.message,
@ -197,11 +200,11 @@ class IRCProtocol extends Protocol {
public resolve(message: IMessage, ...data: any[]): void {
const response = this.parseMessage(...data);
if (!this.irc.alive || !response) {
if (!this.irc.alive || !response || !message.target) {
return;
}
this.irc.message(message.target!.id, response);
this.irc.message(message.target.id, response);
}
public async sendTo(target: string, ...data: any[]): Promise<boolean> {
@ -313,7 +316,7 @@ class IRCServicePlugin extends Plugin {
unloadEventHandler(plugin: string | Plugin): void {
if (plugin === this.name || plugin === this) {
logger.debug('[%s]', this.name, 'shutting down..');
this.service!.stopAll().then(() =>
this.service?.stopAll().then(() =>
this.emit('pluginUnloaded', this));
}
}

View File

@ -1,24 +1,17 @@
import {
Plugin,
EventListener,
Configurable,
InjectService,
Auto
} from '@squeebot/core/lib/plugin';
import { EMessageType, IMessage, IMessageTarget, Protocol } from '@squeebot/core/lib/types';
import { IMessage } from '@squeebot/core/lib/types';
import { logger } from '@squeebot/core/lib/core';
class MyPlugin extends Plugin {
@Auto()
initialize(): void {
}
@EventListener('message')
messageHandler(msg: IMessage): void {
if (msg.data.indexOf('Squeebot') !== -1) {
msg.resolve('Hello %s!', msg.sender!.name);
if (msg.data.indexOf('Squeebot') !== -1 && msg.sender) {
msg.resolve('Hello %s!', msg.sender.name);
}
}

2476
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -13,5 +13,10 @@
"dependencies": {
"@squeebot/core": "^3.3.1",
"typescript": "^4.4.2"
},
"devDependencies": {
"@types/node": "^16.11.13",
"@typescript-eslint/eslint-plugin": "^5.7.0",
"eslint": "^8.4.1"
}
}

View File

@ -3,7 +3,7 @@
"plugins": [
{
"name": "irc",
"version": "1.1.3"
"version": "1.2.0"
},
{
"name": "ircprototest",

View File

@ -1,122 +0,0 @@
{
"extends": "tslint:recommended",
"rules": {
"align": {
"options": [
"parameters",
"statements"
]
},
"array-type": false,
"arrow-return-shorthand": true,
"curly": true,
"deprecation": {
"severity": "warning"
},
"eofline": true,
"import-spacing": true,
"indent": {
"options": [
"spaces"
]
},
"max-classes-per-file": false,
"max-line-length": [
true,
140
],
"member-ordering": [
true,
{
"order": [
"static-field",
"instance-field",
"static-method",
"instance-method"
]
}
],
"no-console": [
true,
"debug",
"info",
"time",
"timeEnd",
"trace"
],
"no-empty": false,
"no-inferrable-types": [
true,
"ignore-params"
],
"no-non-null-assertion": false,
"no-redundant-jsdoc": true,
"no-switch-case-fall-through": true,
"no-var-requires": false,
"object-literal-key-quotes": [
true,
"as-needed"
],
"quotemark": [
true,
"single"
],
"semicolon": {
"options": [
"always"
]
},
"space-before-function-paren": {
"options": {
"anonymous": "never",
"asyncArrow": "always",
"constructor": "never",
"method": "never",
"named": "never"
}
},
"typedef": [
true,
"call-signature"
],
"forin": false,
"ban-types": {
"function": false
},
"typedef-whitespace": {
"options": [
{
"call-signature": "nospace",
"index-signature": "nospace",
"parameter": "nospace",
"property-declaration": "nospace",
"variable-declaration": "nospace"
},
{
"call-signature": "onespace",
"index-signature": "onespace",
"parameter": "onespace",
"property-declaration": "onespace",
"variable-declaration": "onespace"
}
]
},
"variable-name": {
"options": [
"ban-keywords",
"check-format",
"allow-pascal-case"
]
},
"whitespace": {
"options": [
"check-branch",
"check-decl",
"check-operator",
"check-separator",
"check-type",
"check-typecast"
]
}
}
}