びびびどんのスパム対策

最終更新日

自主管理貫徹

 僕が運営しているMastodonサーバーびびびどんは、アクティブユーザー数20人強の小規模サーバーで、「にこにこふわふわしたことをつぶやこう」のサーバールールのもとみんな平和に暮らしている身内鯖なんだけど、Fediverse全体となるとそうもいかない。どうしたって邪悪な人間というのは出てくるのが世の常だ。今年2月には、アカウント作成が容易でモデレーションも十分でないサーバーを狙い撃ちして大量のアカウントを作成し、そこから他サーバーのユーザーに向けて無差別にリプライを送りつけまくる大規模なスパム攻撃が発生した。これまでそういった事例は(少なくとも日本語Fediverseにおいては)ほぼ見られないものだったので、ユーザーも管理人も当惑していたのが印象に残っている。

 そんな中、あるMisskeyサーバーの管理人がグレートエビチリウォールなるスパム対策スクリプトを作成し、コードを公開してくださった。また、これを元に別のMisskeyサーバー管理人も詳細な説明付きでスクリプトを作成・公開してくださったので、これらを大いに参考にしてびびびどんにもスクリプトを導入したところ、幸いにしてスパムは一切サーバーに届かなくなった。なお、これらのスクリプトの仕組みについては、特に後者のページに詳しい。

 しかしながら、今月8日の夜に再度スパム攻撃が発生した。大半のスパムは前述のスクリプトによってサーバー到達前に弾かれていたのだが、一部それを貫通するものもあったため、スクリプトの改修を余儀なくされた。貫通の原因としては、もともとshared inboxに届いたPOSTリクエストについてのみ、スパムかどうか判定する形になっていたのだが、10月の攻撃ではshared inboxではなくユーザーそれぞれのinboxに直接POSTリクエストするものもあったため、それらについては素通りさせてしまっていたのだ。現在は各ユーザーのinboxも監視の対象とし、スパムはめっきり届かなくなった。

 一応、下記にびびびどんで使用しているスクリプトを公開しておく。といっても先述2つめのコードをほぼそのまま流用しているが(2月に流用させていただいたときからかなりの修正があったので、それも適用している)、スパム判定の際にtypeCreateだけでなくUpdateの場合もチェックするようにした。これは、一度リプライのない投稿をした後、その内容をリプライありに修正された場合に対処するためだ。また、今後なぜかスクリプトを貫通した場合に備え、応急的に禁止ワードでも弾くことができるよう、正規表現による判定も加えている。WorkersはCloudflareのダッシュボード上でもコードの修正ができるので、出先で初期対応を迫られたときに便利だ。

export default {
    async fetch(request: Request): Promise<Response> {
        const { pathname } = new URL(request.url)
        if (request.method.toUpperCase() === 'POST' && pathname.endsWith('inbox')) {
            try {
                const body: any = await request.clone().json()
                if (isSpam(body)) {
                    console.log('spam detected ', body.object.id)
                    return new Response(JSON.stringify({error: {message: 'Your request is blocked for spamming.'}}), {status: 202})
                }
            }
            catch (err) {
                console.error('error occured in parsing inbox request body', err)
            }
        }
        return fetch(request)
    },
}

function isSpam(body: any) {
    const spammablePhrase = String(body.object.content).match(/<h1>place some regexes for quick fix<\/h1>/g)
    if (body.type === 'Create' || body.type === 'Update') {
        if (body.cc && body.cc.length > 3) {
            return true
        } else if (spammablePhrase != null) {
            console.log(body)
            return true
        } else {
            return false
        }
    } else {
        return false
    }
}