Anatomi Masalah: Sifat Webmention dan Risiko Backend
Webmention adalah rekomendasi W3C untuk notifikasi terdesentralisasi antar-situs. Ketika seseorang menautkan artikel Anda di situs mereka, server mereka mengirimkan HTTP POST ke endpoint Webmention Anda dengan membawa dua parameter: source (URL mereka) dan target (URL Anda). Server Anda kemudian wajib memverifikasi bahwa source benar-benar memuat tautan ke target sebelum menyimpannya sebagai mention, komentar, atau repost.
Mengabaikan arsitektur pemrosesan yang tepat menimbulkan dua kerentanan kritis:
- Denial of Service (DoS) melalui HTTP hanging: Melakukan HTTP GET ke
sourcesecara sinkron di dalam thread web request akan memblokir worker koneksi. Penyerang dapat memasukkan ribuan source lambat (Slowloris/Tarpit) hingga thread pool habis. - Server-Side Request Forgery (SSRF): Menjadikan
sourcesebagai target pengambilan data mentah memungkinkan penyerang menunjuk alamat internal sepertihttp://169.254.169.254/(metadata instance cloud) atau subnet LAN pribadihttp://10.0.0.1/.
Kontrak API: Respon 202 Accepted dan Validasi Awal
Spesifikasi Webmention menginstruksikan penerima untuk segera mengembalikan status HTTP 202 Accepted jika payload sintaksis valid dan pemrosesan dilakukan secara asinkron.
Validasi tahap pertama yang wajib selesai sebelum status 202 dikembalikan meliputi:
- Payload bertipe
application/x-www-form-urlencoded. - Parameter
sourcedantargetterisi dan berupa URL absolut berprotokolhttpatauhttps. - URL
targetmerujuk ke domain lokal milik Anda dan entitas resource yang dituju memang valid di database. - Parameter
sourcetidak sama persis dengantarget(mencegah self-referencing loops).
Contoh Handler Endpoint (Express.js / Node.js)
import { Request, Response } from 'express';
import { createHash } from 'node:crypto';
import { mentionQueue } from './queue.js';
import { isLocalHostTarget } from './url-validator.js';
export async function webmentionHandler(req: Request, res: Response): Promise<void> {
const { source, target } = req.body;
if (!source || !target) {
res.status(400).json({ error: 'Missing source or target' });
return;
}
let sourceUrl: URL;
let targetUrl: URL;
try {
sourceUrl = new URL(source);
targetUrl = new URL(target);
} catch {
res.status(400).json({ error: 'Invalid URL format' });
return;
}
if (!['http:', 'https:'].includes(sourceUrl.protocol) || !['http:', 'https:'].includes(targetUrl.protocol)) {
res.status(400).json({ error: 'Unsupported protocol' });
return;
}
if (sourceUrl.href === targetUrl.href) {
res.status(400).json({ error: 'Source and target must differ' });
return;
}
if (!isLocalHostTarget(targetUrl)) {
res.status(400).json({ error: 'Target URL is not hosted on this server' });
return;
}
// Idempotency check via Hash
const hashKey = createHash('sha256').update(`${sourceUrl.href}:${targetUrl.href}`).digest('hex');
// Kirim ke antrean background
await mentionQueue.add('verify-webmention', {
source: sourceUrl.href,
target: targetUrl.href,
idempotencyKey: hashKey,
}, {
jobId: hashKey, // Cegah duplikasi job aktif
removeOnComplete: true,
});
res.status(202).send('Accepted');
}
Idempotensi Payload: Mengatasi Duplicate Mentions dan Spam
Pengirim Webmention sering melakukan pengiriman berulang ketika artikel di-update atau ketika koneksi jaringan timeout. Tanpa idempotensi, entitas Webmention dapat tercatat ganda pada feed komentar.
Kunci idempotensi didefinisikan secara deterministik menggunakan operasi ringkasan pesan (hash):
Idempotency Key = SHA256(normalize(source) + ":" + normalize(target))Normalisasi mencakup penurunan huruf domain (lowercasing host), penghapusan trailing slash yang redundan, dan pembersihan tracking query strings (seperti utm_*). Database harus menerapkan unique constraint gabungan pada (source_hash, target_hash) atau menyimpan kunci di in-memory store (Redis) dengan status siklus hidup:
PENDING: Webmention sedang berada di antrean verifikasi. Payload baru yang identik dibatalkan dengan mengembalikan 202 tanpa menjadwalkan job baru.VERIFIED: Source valid dan tautan telah dipublikasikan. Permintaan baru akan memicu re-verifikasi update konten.FAILED: Verifikasi gagal (misal link dihapus). Diberikan batas waktu retensi (TTL) agar tidak membebani server oleh bot retry.
Mitigasi SSRF pada Outbound Fetching
Saat worker asinkron memvalidasi source, worker tersebut melakukan HTTP GET ke server eksternal. Penyerang dapat memasukkan URL yang mengarah ke intranet atau IP metadata cloud (misal AWS/GCP 169.254.169.254). Selain itu, teknik DNS Rebinding dapat melewati validasi string URL konvensional.
Validasi yang aman mewajibkan pengujian IP address langsung saat resolusi DNS, bukan sekadar memeriksa hostname.
Daftar Rentang IP Terlarang
- Loopback:
127.0.0.0/8,::1/128 - Private Subnets:
10.0.0.0/8,172.16.0.0/12,192.168.0.0/16 - Link-Local / Cloud Metadata:
169.254.0.0/16,fe80::/10 - Carrier-Grade NAT:
100.64.0.0/10
Implementasi Custom HTTP Agent Anti-SSRF
Contoh berikut menggunakan library ipaddr.js pada Node.js untuk mengevaluasi alamat IP hasil resolusi DNS sebelum soket TCP terbentuk:
import http from 'node:http';
import https from 'node:https';
import dns from 'node:dns/promises';
import ipaddr from 'ipaddr.js';
function isForbiddenIp(ipString: string): boolean {
const addr = ipaddr.parse(ipString);
const range = addr.range();
const blockedRanges = [
'loopback',
'private',
'linkLocal',
'carrierGradeNat',
'uniqueLocal',
'unspecified',
];
return blockedRanges.includes(range);
}
// Custom lookup untuk pinning IP aman dan memblokir DNS Rebinding
export async function safeLookup(hostname: string, options: any, callback: Function) {
try {
const addresses = await dns.lookup(hostname, { all: true });
for (const record of addresses) {
if (isForbiddenIp(record.address)) {
return callback(new Error(`SSRF Blocked: IP ${record.address} is forbidden`), null, null);
}
}
// Ambil alamat pertama yang lolos inspeksi
const selected = addresses[0];
callback(null, selected.address, selected.family);
} catch (err) {
callback(err, null, null);
}
}
Peringatan Keamanan: Jangan mengandalkan modul bawaan fetch yang mengikuti HTTP Redirects secara buta (
redirect: 'follow'). Pengirim dapat mengarahkansourcedomain publik ke internal IP via HTTP 302. Tangani redirect manual: batas maksimal 3 hop, dan setiap URL tujuan wajib melaluisafeLookupkembali.
Strategi Antrean Asinkron & Verifikasi HTML
Setelah koneksi aman terbangun, worker memproses verifikasi konten dengan aturan ketat.
1. Pembatasan Resource Download
- Gunakan
AbortSignal.timeout(5000)untuk membatasi durasi request maksimal 5 detik. - Batasi ukuran respons (streaming limit) maksimal 1 MB hingga 2 MB. Jangan memuat dokumen HTML tak terbatas ke dalam memori untuk menghindari Zip-bomb atau memory exhaustion.
- Hanya parse respons dengan header
Content-Type: text/htmlatauapplication/xhtml+xml.
2. Algoritma Pencarian Tautan Target
Parse dokumen HTML menggunakan parser parser berbasis stream atau parser DOM cepat (seperti cheerio / htmlparser2). Periksa elemen berikut:
- Tautan standar:
<a href="TARGET_URL"> - Tautan microformats2:
<a class="u-in-reply-to" href="TARGET_URL">atauu-like-of,u-repost-of.
Jika tautan ditemukan, simpan mention dan ekstrak metadata pengirim (author name, avatar, bio). Jika tidak ditemukan, anggap mention tidak valid atau telah ditarik (unmention).
3. Konfigurasi Worker Queue dan Exponential Backoff
Jika server pengirim mengalami 5xx server error atau koneksi reset, worker harus mencoba kembali secara terukur (retry policy) tanpa membanjiri target:
| Percobaan | Delay Interval | Tindakan Jika Gagal |
|---|---|---|
| Retry 1 | 30 Detik | Masuk ulang antrean worker |
| Retry 2 | 5 Menit | Masuk ulang antrean worker |
| Retry 3 | 30 Menit | Masuk ulang antrean worker |
| Final | - | Tandai status FAILED, catat log metrik |
Kesimpulan
Penerimaan Webmention yang aman menuntut isolasi ketat antara penerimaan webhook dan eksekusi jaringan outbound. Mengembalikan status 202 Accepted langsung menjaga ketersediaan thread web, penggunaan hash idempotensi mengeliminasi payload ganda, dan validasi IP tingkat DNS mencegah celah SSRF serta eksploitasi infrastruktur internal.
Komentar
0 komentar
Masuk ke akun kamu untuk ikut berkomentar.
Belum ada komentar
Jadilah yang pertama ikut berdiskusi!