DoS
サーバやネットワークなどに意図的に過剰な負荷をかけたり脆弱性をついたりすることでサービスの可用性に影響を与える攻撃
例
以下はheap領域を大量に使用させ、結果としてbotをクラッシュさせます。
code: dice-bot.js
const { Client } = require("discord.js");
const client = new Client({
allowedMentions: {
parse: []
}
});
const token = process.env.DISCORD_TOKEN;
async function onMessage(message) {
const arr = message.content.split("d");
if (arr.length != 2) {
return;
}
const num, max = arr.map(e => Number.parseInt(e, 10)); if (Number.isNaN(num) && Number.isNaN(max)) {
return;
}
const roll_result = new Array(num).fill(null).map(() => Math.floor(Math.random() * max) + 1);
const sum = roll_result.reduce((acc, cur) => acc + cur);
await message.channel.send(num > 24 ? 合計:${String(sum)} : 合計:${String(sum)}\n${roll_result.join(",")});
}
client.on("message", (message) => {
onMessage(message).catch((err) => console.error(err));
});
client.login(token);
https://gyazo.com/eb92a203f21b1ae49aaf9caaba04136f
https://gyazo.com/6fad2834e93d0fcfacbd31a7925dd7b1
対策
個数の検証を行う
code:dice-bot.js
const { Client } = require("discord.js");
const client = new Client({
allowedMentions: {
parse: []
}
});
const token = process.env.DISCORD_TOKEN;
async function onMessage(message) {
const arr = message.content.split("d");
if (arr.length != 2) {
return;
}
const num, max = arr.map(e => Number.parseInt(e, 10)); if (Number.isNaN(num) && Number.isNaN(max)) {
return;
}
//追加
if (num >= 10000) {
await message.channel.send("サイコロの個数は10000個以下とする必要があります。");
return;
}
const roll_result = new Array(num).fill(null).map(() => Math.floor(Math.random() * max) + 1);
const sum = roll_result.reduce((acc, cur) => acc + cur);
await message.channel.send(num > 24 ? 合計:${String(sum)} : 合計:${String(sum)}\n${roll_result.join(",")});
}
client.on("message", (message) => {
onMessage(message).catch((err) => console.error(err));
});
client.login(token);
アルゴリズムを改良して高速に動作させる
O(num)→O(1)
code:dice-bot.js
const { Client } = require("discord.js");
const client = new Client({
allowedMentions: {
parse: []
}
});
function rnorm() {
return Math.sqrt(-2 * Math.log(1 - Math.random())) * Math.cos(2 * Math.PI * Math.random());
}
function calc_V(mean, max) {
return -(max * (max + 1) * (max + 2) / 6 - mean * max * (max + 1) + max * mean * mean) / max;
}
function dice2(num, max) {
const max_result = num * max;
if (!Number.isSafeInteger(max_result)) {
throw new TypeError("too large input!");
}
const mean = (max + 1) / 2;
const v = calc_V(mean, max);
const sigma = Math.sqrt(v / num);
while (true) {
const result = Math.round((sigma * rnorm() + mean) * num);
if (num <= result && result <= max_result) {
return result;
}
}
}
const token = process.env.DISCORD_TOKEN;
async function onMessage(message) {
const arr = message.content.split("d");
if (arr.length != 2) {
return;
}
const num, max = arr.map(e => Number.parseInt(e, 10)); if (Number.isNaN(num) && Number.isNaN(max)) {
return;
}
//追加
if (num >= 10000) {
await message.channel.send(合計:${String(dice2(num, max))});
return;
}
const roll_result = new Array(num).fill(null).map(() => Math.floor(Math.random() * max) + 1);
const sum = roll_result.reduce((acc, cur) => acc + cur);
await message.channel.send(num > 24 ? 合計:${String(sum)} : 合計:${String(sum)}\n${roll_result.join(",")});
}
client.on("message", (message) => {
onMessage(message).catch((err) => console.error(err));
});
client.login(token);