p-hone p-hone.info

Misskey 対スパム 真剣勝負

2024/10/10: shared inbox以外の場合を考慮して一部加筆修正

2024年2月16日ごろから、分散SNSの一部でスパム投稿の被害が増えている。 機械的に作ったアカウントで意味のないメッセージを無差別に飛ばすというものだ。

私の個人Misskeyサーバー moeanime.io にもやはり飛んできた。 個人サーバーの場合、自身が管理者なので不要な投稿は削除できる。 最初は見つけ次第スパム投稿を消していた。 しかし、数日間たってもスパムは止むことがなく削除しつづけるのも疲れるので対策を考えることにした。

面倒なことになったと同時に、すこし楽しくなってきた。 たとえばTwitter(X)のような中央集権的SNSで同じ事が起きたら「運営しっかり対策してくれ!」と不満の声を上げるぐらいしかできない。 だが、分散SNS…特に個人サーバーの場合は自分自身が運営であり自らの行動ですべてをコントロールできる。

ほとんどの分散型SNSはソースコードや通信規格が公開されているため、攻撃側(スパム)も防御側(サーバー管理者)も享受できる情報の格差はない。つまり、私とスパム送信者で知識量と技術力の真剣勝負だ!パソコン大好きオタクとして闘志が湧いてきた。

心強いことにサーバー管理者はたくさんいて、運営に有用な情報を発信してくれている。 そこでグレートエビチリウォールというツールを知った。 サーバーに飛んでくる投稿を届く前にチェックしてNGワードが入っていれば捨てる、という仕組みだ。

分散SNS(ActivityPub)では各ユーザーがInbox(受信箱)を持っていて、外部からの投稿はInboxに入る。 それを利用した対策法である。


https://w3c.github.io/activitypub/#Overview より引用

たとえば https://moeanime.io サーバーのユーザーID xxx のInboxは https://moeanime.io/users/xxx/inbox となる。 また、サーバーに多くのユーザーがいる場合に一人ずつの受信箱に投稿すると通信が膨大になるため、サーバー全体で共通の受信箱(Shared Inbox)を通す仕組み1もある。

つまり、https://moeanime.io の場合、外部からの投稿は https://moeanime.io/users/xxx/inbox または https://moeanime.io/inbox に届く。 ここで明らかにスパムなものは捨ててしまえばいい。

グレートエビチリウォールはinboxのチェック処理にCloudflare Workersを使っている。Workersはサーバーに来るアクセスの中継・改変が可能なので、こういう用途に向いているサービスだ。個人サーバー程度のアクセス数なら無料で使えるのも嬉しい。

仕組みをざっくり理解したので、私も簡単なスパム対策のWorkerを作った。 Cloudflare Workersのガイドの通り新しいアプリを作り、 src/index.ts を次のように書いた。 ここでは リプライが3つ以上付いている投稿をスパムとみなす 判定とした2

// src/index.ts
export default {
    async fetch(request: Request): Promise<Response> {
        const { pathname } = new URL(request.url)
        // URLの末尾が /inbox の場合のみスパム判定
        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: 'blocked by validator'}}), {status: 202})
                }
            }
            catch (err) {
                console.error('error occured in parsing inbox request body', err)
            }
        }
        return fetch(request)
    },
}

function isSpam(body: any) {
    // リプライが3つ以上ついているノート投稿をスパムとみなす
    // ccの1つめは followers が入っているので、>=3 ではなく >3 にしている
    return body.type === 'Create' && body.cc && body.cc.length > 3
}

これを自身のCloudflareアカウントにデプロイして、 Workers Routes という機能を使って moeanime.io/* (すべてのアクセス) がこのWorkersを通るように設定した。

反映後、しばらくWorkerのログを眺めていたが、思い通りいくつかのスパム投稿が検出できていた。 Workerによって捨てられたため、SNSのアカウントには届いていない。大勝利!

この後、攻撃側がやり方を変えてきて再び被害を受ける可能性ももちろんある。 そうなれば再びこちらも新たな防御策を考えるまでだ。これは知識量と技術力の真剣勝負である!

こんなバトルに貴重な時間を割くのはどうなのかという気持ちもなくはないが、 この勝負を通してCloudflare Workersや分散SNSの仕組みを勉強するきっかけとなったし、ブログのネタにもできたので不満はない。

脚注

  1. https://w3c.github.io/activitypub/#shared-inbox-delivery

  2. 誤判定の可能性も考えたが、少なくとも自分のフォロー内では3人以上にリプライをつける会話を見たことがないので十分と判断した。