\n \n\n```\n\n**Protection:**\n\n```javascript\nconst sanitizeHtml = require('sanitize-html');\n\nfunction sanitizeContent(html) {\n return sanitizeHtml(html, {\n allowedTags: ['p', 'br', 'strong', 'em', 'a', 'blockquote', 'code'],\n allowedAttributes: {\n 'a': ['href', 'title']\n },\n allowedSchemes: ['http', 'https', 'mailto'],\n });\n}\n\n// When displaying\nfunction displayMention(mention) {\n const safeContent = sanitizeContent(mention.content.html);\n return `
${safeContent}
`;\n}\n```\n\n### 3. URL Validation\n\nEnsure URLs are properly formed:\n\n```javascript\nfunction validateUrl(url) {\n try {\n const parsed = new URL(url);\n\n // Must be HTTP(S)\n if (!['http:', 'https:'].includes(parsed.protocol)) {\n return false;\n }\n\n // Must have valid hostname\n if (!parsed.hostname || parsed.hostname.length === 0) {\n return false;\n }\n\n return true;\n } catch {\n return false;\n }\n}\n```\n\n### 4. Rate Limiting\n\nPrevent abuse:\n\n```javascript\nconst rateLimit = require('express-rate-limit');\n\n// IP-based limiting\nconst ipLimiter = rateLimit({\n windowMs: 15 * 60 * 1000, // 15 minutes\n max: 20, // 20 requests per window\n message: 'Too many requests from this IP',\n});\n\n// Domain-based limiting\nconst domainLimiters = new Map();\n\nfunction checkDomainLimit(source) {\n const domain = new URL(source).hostname;\n const limiter = domainLimiters.get(domain) || { count: 0, resetAt: Date.now() + 3600000 };\n\n if (Date.now() > limiter.resetAt) {\n limiter.count = 0;\n limiter.resetAt = Date.now() + 3600000;\n }\n\n limiter.count++;\n domainLimiters.set(domain, limiter);\n\n if (limiter.count > 50) { // 50 mentions per hour per domain\n throw new Error('Domain rate limit exceeded');\n }\n}\n\napp.post('/webmention', ipLimiter, handleWebmention);\n```\n\n## Privacy Considerations\n\n### What Data is Shared?\n\nWhen you send a Webmention, you share:\n\n- **Source URL**: The page mentioning them\n- **Target URL**: The page being mentioned\n- **Public content**: Anything on your source page\n- **Server metadata**: IP address, user agent (in HTTP request)\n\n**Not shared:**\n- Private/unpublished content\n- Analytics data\n- User information (unless on the page)\n- Internal site structure\n\n### Receiving Webmentions Privately\n\nSome sites don't want to display all mentions publicly:\n\n```javascript\n// Save mention but don't display\nasync function processMention(source, target, data) {\n await db.mentions.insert({\n source,\n target,\n ...data,\n visibility: 'private', // Only owner sees it\n verified: true,\n });\n}\n\n// API only returns public mentions to visitors\napp.get('/api/mentions', (req, res) => {\n const isOwner = req.user && req.user.isOwner;\n\n const query = {\n target: req.query.target,\n verified: true,\n };\n\n // Non-owners only see public mentions\n if (!isOwner) {\n query.visibility = { $ne: 'private' };\n }\n\n const mentions = await db.mentions.find(query);\n res.json(mentions);\n});\n```\n\n### Anonymous Mentions\n\nAllow mentions without revealing identity:\n\n```javascript\n// Support anonymous h-card\nfunction parseAuthor(hcard) {\n // If no name provided, use \"Anonymous\"\n const name = hcard.properties.name?.[0] || 'Anonymous';\n\n // If no photo, use default avatar\n const photo = hcard.properties.photo?.[0] || '/images/default-avatar.png';\n\n // URL is optional\n const url = hcard.properties.url?.[0];\n\n return { name, photo, url };\n}\n```\n\n### Selective Display\n\nChoose which mentions to show:\n\n```javascript\n// Moderation statuses\nconst STATUSES = {\n PENDING: 'pending', // Awaiting review\n APPROVED: 'approved', // Public display\n SPAM: 'spam', // Hidden spam\n PRIVATE: 'private', // Received but not shown\n};\n\n// Display policy\nfunction shouldDisplay(mention, context) {\n // Never show spam\n if (mention.status === 'spam') return false;\n\n // Owners see everything except spam\n if (context.isOwner) return true;\n\n // Public only sees approved\n return mention.status === 'approved';\n}\n```\n\n## GDPR and Data Protection\n\n### Right to Be Forgotten\n\nImplement deletion:\n\n```javascript\n// Delete endpoint\napp.delete('/webmention/:id', requireAuth, async (req, res) => {\n await db.mentions.delete({ _id: req.params.id });\n res.send('Deleted');\n});\n\n// Handle source deletion\nasync function handleSourceGone(source) {\n // When source returns 410 Gone, delete mentions\n await db.mentions.updateMany(\n { source },\n { status: 'deleted', deletedAt: new Date() }\n );\n}\n```\n\n### Update Handling\n\nRespect mention updates:\n\n```javascript\nasync function updateMention(source, target) {\n const existing = await db.mentions.findOne({ source, target });\n\n if (!existing) {\n // New mention\n await createMention(source, target);\n return;\n }\n\n // Re-verify and update\n const data = await fetchAndParse(source, target);\n\n if (!data) {\n // Link removed, delete mention\n await db.mentions.updateOne(\n { _id: existing._id },\n { status: 'deleted', deletedAt: new Date() }\n );\n } else {\n // Update existing mention\n await db.mentions.updateOne(\n { _id: existing._id },\n { ...data, updatedAt: new Date() }\n );\n }\n}\n```\n\n### Data Minimization\n\nOnly store necessary data:\n\n```javascript\n// ✅ Minimal storage\n{\n source: 'https://...',\n target: 'https://...',\n authorName: 'Alice',\n authorPhoto: 'https://...',\n content: 'Great post!',\n published: '2024-01-15',\n type: 'reply',\n verified: true,\n}\n\n// ❌ Excessive storage\n{\n // Don't store full HTML\n sourceHtml: '...',\n // Don't store IP addresses\n requesterIp: '192.168...',\n // Don't store user agent\n userAgent: 'Mozilla...',\n // Don't store raw microformats\n rawMicroformats: {...},\n}\n```\n\n## Content Security Policy (CSP)\n\nProtect against XSS when displaying mentions:\n\n```html\n\n```\n\n## HTTPS Requirements\n\nAlways use HTTPS:\n\n```javascript\n// Reject non-HTTPS URLs (optional but recommended)\nfunction requireHttps(url) {\n const parsed = new URL(url);\n\n if (parsed.protocol !== 'https:') {\n throw new Error('HTTPS required');\n }\n}\n\n// Or upgrade HTTP to HTTPS automatically\nfunction upgradeToHttps(url) {\n return url.replace(/^http:/, 'https:');\n}\n```\n\n## Vouch for Enhanced Trust\n\nImplement the Vouch extension:\n\n```javascript\n// Require vouch from unknown domains\nconst trustedDomains = new Set(['indieweb.org', 'example.com']);\n\nasync function checkVouch(source, vouch) {\n // Vouch must be from trusted domain\n const vouchDomain = new URL(vouch).hostname;\n if (!trustedDomains.has(vouchDomain)) {\n return false;\n }\n\n // Vouch page must link to source\n const response = await fetch(vouch);\n const html = await response.text();\n return html.includes(source);\n}\n\napp.post('/webmention', async (req, res) => {\n const { source, target, vouch } = req.body;\n const sourceDomain = new URL(source).hostname;\n\n // Unknown domains need vouch\n if (!trustedDomains.has(sourceDomain)) {\n if (!vouch || !await checkVouch(source, vouch)) {\n return res.status(449).send('Vouch required');\n }\n }\n\n // Process normally\n await processMention(source, target);\n res.status(202).send('Accepted');\n});\n```\n\n## Audit Logging\n\nTrack Webmention activity:\n\n```javascript\nasync function logWebmention(action, data) {\n await db.logs.insert({\n action, // 'received', 'verified', 'approved', 'rejected'\n timestamp: new Date(),\n source: data.source,\n target: data.target,\n ip: data.ip,\n result: data.result,\n });\n}\n\n// Use in processing\nasync function processWebmention(req) {\n await logWebmention('received', {\n source: req.body.source,\n target: req.body.target,\n ip: req.ip,\n });\n\n const verified = await verify(req.body);\n\n await logWebmention('verified', {\n ...req.body,\n result: verified,\n });\n}\n```\n\n## Security Checklist\n\n- [ ] Verify all mentions before storing\n- [ ] Process asynchronously\n- [ ] Block private IP ranges (SSRF protection)\n- [ ] Sanitize HTML content (XSS protection)\n- [ ] Validate URLs properly\n- [ ] Implement rate limiting\n- [ ] Use HTTPS exclusively\n- [ ] Set appropriate CSP headers\n- [ ] Handle updates and deletions\n- [ ] Minimize data storage\n- [ ] Log security events\n- [ ] Regular security audits\n\n## Privacy Checklist\n\n- [ ] Respect deletion requests\n- [ ] Support private mentions\n- [ ] Allow anonymous mentions\n- [ ] Implement moderation\n- [ ] Clear privacy policy\n- [ ] GDPR compliance\n- [ ] User consent for data usage\n- [ ] Secure data storage\n\n## Best Practices\n\n1. **Defense in depth**: Multiple security layers\n2. **Principle of least privilege**: Only store necessary data\n3. **Fail securely**: Reject on error, don't approve\n4. **Regular updates**: Keep dependencies current\n5. **Security testing**: Regular penetration testing\n6. **Incident response**: Plan for security events\n\nWebmentions can be both secure and privacy-respecting when implemented with care. The protocol's design encourages good practices, but proper implementation is essential.\n"}}]}
Home

/

FAQ

/

Are Webmentions secure and private?

Are Webmentions secure and private?

Understanding the security and privacy implications of Webmentions, including common concerns and how to protect your site and users.

Webmentions are designed with security in mind, but like any web technology, they require proper implementation and understanding to keep your site and users safe.

Security Fundamentals

The Verification Requirement

The most important security feature is mandatory verification:

// ❌ NEVER do this - accepting without verification
app.post('/webmention', (req, res) => {
  const { source, target } = req.body;
  saveMention(source, target); // Dangerous!
  res.send('OK');
});

// ✅ ALWAYS verify
app.post('/webmention', async (req, res) => {
  const { source, target } = req.body;

  // Accept immediately
  res.status(202).send('Accepted');

  // Verify asynchronously
  const isValid = await verifyLink(source, target);
  if (isValid) {
    saveMention(source, target);
  }
});

This prevents:

  • Fake mentions from non-existent pages
  • Spam claiming to link when they don't
  • Impersonation attempts

Async Processing

Always process Webmentions asynchronously:

Why? Fetching the source URL synchronously creates attack vectors:

// ❌ Vulnerable to slowloris attacks
app.post('/webmention', async (req, res) => {
  await fetch(req.body.source); // Attacker can make this slow!
  res.send('OK'); // Ties up your server
});

// ✅ Protected
app.post('/webmention', async (req, res) => {
  await queue.add(req.body); // Queue it
  res.status(202).send('Accepted'); // Return immediately
});

// Process in background worker
queue.process(async (job) => {
  await verifyMention(job.data);
});

Common Security Threats

1. SSRF (Server-Side Request Forgery)

Attackers could try to make your server fetch internal URLs:

// Malicious mention
{
  source: "http://localhost:6379/", // Redis
  target: "https://yoursite.com/post"
}

Protection:

function isPublicUrl(url) {
  const parsed = new URL(url);
  const hostname = parsed.hostname;

  // Block private IPs
  const privateRanges = [
    /^localhost$/i,
    /^127\./,
    /^10\./,
    /^172\.(1[6-9]|2[0-9]|3[01])\./,
    /^192\.168\./,
    /^169\.254\./, // Link-local
    /^::1$/, // IPv6 localhost
    /^fe80::/i, // IPv6 link-local
  ];

  if (privateRanges.some(range => range.test(hostname))) {
    throw new Error('Private IP not allowed');
  }

  // Block cloud metadata endpoints
  if (hostname === '169.254.169.254') {
    throw new Error('Cloud metadata endpoint blocked');
  }

  // Only allow HTTP/HTTPS
  if (!['http:', 'https:'].includes(parsed.protocol)) {
    throw new Error('Invalid protocol');
  }

  return true;
}

2. XSS (Cross-Site Scripting)

Malicious content in mentions:

<!-- Attacker's page -->
<div class="h-entry">
  <div class="e-content">
    <script>alert('XSS')</script>
  </div>
</div>

Protection:

const sanitizeHtml = require('sanitize-html');

function sanitizeContent(html) {
  return sanitizeHtml(html, {
    allowedTags: ['p', 'br', 'strong', 'em', 'a', 'blockquote', 'code'],
    allowedAttributes: {
      'a': ['href', 'title']
    },
    allowedSchemes: ['http', 'https', 'mailto'],
  });
}

// When displaying
function displayMention(mention) {
  const safeContent = sanitizeContent(mention.content.html);
  return `<div class="mention">${safeContent}</div>`;
}

3. URL Validation

Ensure URLs are properly formed:

function validateUrl(url) {
  try {
    const parsed = new URL(url);

    // Must be HTTP(S)
    if (!['http:', 'https:'].includes(parsed.protocol)) {
      return false;
    }

    // Must have valid hostname
    if (!parsed.hostname || parsed.hostname.length === 0) {
      return false;
    }

    return true;
  } catch {
    return false;
  }
}

4. Rate Limiting

Prevent abuse:

const rateLimit = require('express-rate-limit');

// IP-based limiting
const ipLimiter = rateLimit({
  windowMs: 15 * 60 * 1000, // 15 minutes
  max: 20, // 20 requests per window
  message: 'Too many requests from this IP',
});

// Domain-based limiting
const domainLimiters = new Map();

function checkDomainLimit(source) {
  const domain = new URL(source).hostname;
  const limiter = domainLimiters.get(domain) || { count: 0, resetAt: Date.now() + 3600000 };

  if (Date.now() > limiter.resetAt) {
    limiter.count = 0;
    limiter.resetAt = Date.now() + 3600000;
  }

  limiter.count++;
  domainLimiters.set(domain, limiter);

  if (limiter.count > 50) { // 50 mentions per hour per domain
    throw new Error('Domain rate limit exceeded');
  }
}

app.post('/webmention', ipLimiter, handleWebmention);

Privacy Considerations

What Data is Shared?

When you send a Webmention, you share:

  • Source URL: The page mentioning them
  • Target URL: The page being mentioned
  • Public content: Anything on your source page
  • Server metadata: IP address, user agent (in HTTP request)

Not shared:

  • Private/unpublished content
  • Analytics data
  • User information (unless on the page)
  • Internal site structure

Receiving Webmentions Privately

Some sites don't want to display all mentions publicly:

// Save mention but don't display
async function processMention(source, target, data) {
  await db.mentions.insert({
    source,
    target,
    ...data,
    visibility: 'private', // Only owner sees it
    verified: true,
  });
}

// API only returns public mentions to visitors
app.get('/api/mentions', (req, res) => {
  const isOwner = req.user && req.user.isOwner;

  const query = {
    target: req.query.target,
    verified: true,
  };

  // Non-owners only see public mentions
  if (!isOwner) {
    query.visibility = { $ne: 'private' };
  }

  const mentions = await db.mentions.find(query);
  res.json(mentions);
});

Anonymous Mentions

Allow mentions without revealing identity:

// Support anonymous h-card
function parseAuthor(hcard) {
  // If no name provided, use "Anonymous"
  const name = hcard.properties.name?.[0] || 'Anonymous';

  // If no photo, use default avatar
  const photo = hcard.properties.photo?.[0] || '/images/default-avatar.png';

  // URL is optional
  const url = hcard.properties.url?.[0];

  return { name, photo, url };
}

Selective Display

Choose which mentions to show:

// Moderation statuses
const STATUSES = {
  PENDING: 'pending',     // Awaiting review
  APPROVED: 'approved',   // Public display
  SPAM: 'spam',          // Hidden spam
  PRIVATE: 'private',    // Received but not shown
};

// Display policy
function shouldDisplay(mention, context) {
  // Never show spam
  if (mention.status === 'spam') return false;

  // Owners see everything except spam
  if (context.isOwner) return true;

  // Public only sees approved
  return mention.status === 'approved';
}

GDPR and Data Protection

Right to Be Forgotten

Implement deletion:

// Delete endpoint
app.delete('/webmention/:id', requireAuth, async (req, res) => {
  await db.mentions.delete({ _id: req.params.id });
  res.send('Deleted');
});

// Handle source deletion
async function handleSourceGone(source) {
  // When source returns 410 Gone, delete mentions
  await db.mentions.updateMany(
    { source },
    { status: 'deleted', deletedAt: new Date() }
  );
}

Update Handling

Respect mention updates:

async function updateMention(source, target) {
  const existing = await db.mentions.findOne({ source, target });

  if (!existing) {
    // New mention
    await createMention(source, target);
    return;
  }

  // Re-verify and update
  const data = await fetchAndParse(source, target);

  if (!data) {
    // Link removed, delete mention
    await db.mentions.updateOne(
      { _id: existing._id },
      { status: 'deleted', deletedAt: new Date() }
    );
  } else {
    // Update existing mention
    await db.mentions.updateOne(
      { _id: existing._id },
      { ...data, updatedAt: new Date() }
    );
  }
}

Data Minimization

Only store necessary data:

// ✅ Minimal storage
{
  source: 'https://...',
  target: 'https://...',
  authorName: 'Alice',
  authorPhoto: 'https://...',
  content: 'Great post!',
  published: '2024-01-15',
  type: 'reply',
  verified: true,
}

// ❌ Excessive storage
{
  // Don't store full HTML
  sourceHtml: '<html>...</html>',
  // Don't store IP addresses
  requesterIp: '192.168...',
  // Don't store user agent
  userAgent: 'Mozilla...',
  // Don't store raw microformats
  rawMicroformats: {...},
}

Content Security Policy (CSP)

Protect against XSS when displaying mentions:

<meta http-equiv="Content-Security-Policy"
      content="default-src 'self';
               connect-src 'self' https://api.webmentions.com;
               img-src 'self' https:;
               style-src 'self' 'unsafe-inline';
               script-src 'self';">

HTTPS Requirements

Always use HTTPS:

// Reject non-HTTPS URLs (optional but recommended)
function requireHttps(url) {
  const parsed = new URL(url);

  if (parsed.protocol !== 'https:') {
    throw new Error('HTTPS required');
  }
}

// Or upgrade HTTP to HTTPS automatically
function upgradeToHttps(url) {
  return url.replace(/^http:/, 'https:');
}

Vouch for Enhanced Trust

Implement the Vouch extension:

// Require vouch from unknown domains
const trustedDomains = new Set(['indieweb.org', 'example.com']);

async function checkVouch(source, vouch) {
  // Vouch must be from trusted domain
  const vouchDomain = new URL(vouch).hostname;
  if (!trustedDomains.has(vouchDomain)) {
    return false;
  }

  // Vouch page must link to source
  const response = await fetch(vouch);
  const html = await response.text();
  return html.includes(source);
}

app.post('/webmention', async (req, res) => {
  const { source, target, vouch } = req.body;
  const sourceDomain = new URL(source).hostname;

  // Unknown domains need vouch
  if (!trustedDomains.has(sourceDomain)) {
    if (!vouch || !await checkVouch(source, vouch)) {
      return res.status(449).send('Vouch required');
    }
  }

  // Process normally
  await processMention(source, target);
  res.status(202).send('Accepted');
});

Audit Logging

Track Webmention activity:

async function logWebmention(action, data) {
  await db.logs.insert({
    action, // 'received', 'verified', 'approved', 'rejected'
    timestamp: new Date(),
    source: data.source,
    target: data.target,
    ip: data.ip,
    result: data.result,
  });
}

// Use in processing
async function processWebmention(req) {
  await logWebmention('received', {
    source: req.body.source,
    target: req.body.target,
    ip: req.ip,
  });

  const verified = await verify(req.body);

  await logWebmention('verified', {
    ...req.body,
    result: verified,
  });
}

Security Checklist

  • Verify all mentions before storing
  • Process asynchronously
  • Block private IP ranges (SSRF protection)
  • Sanitize HTML content (XSS protection)
  • Validate URLs properly
  • Implement rate limiting
  • Use HTTPS exclusively
  • Set appropriate CSP headers
  • Handle updates and deletions
  • Minimize data storage
  • Log security events
  • Regular security audits

Privacy Checklist

  • Respect deletion requests
  • Support private mentions
  • Allow anonymous mentions
  • Implement moderation
  • Clear privacy policy
  • GDPR compliance
  • User consent for data usage
  • Secure data storage

Best Practices

  1. Defense in depth: Multiple security layers
  2. Principle of least privilege: Only store necessary data
  3. Fail securely: Reject on error, don't approve
  4. Regular updates: Keep dependencies current
  5. Security testing: Regular penetration testing
  6. Incident response: Plan for security events

Webmentions can be both secure and privacy-respecting when implemented with care. The protocol's design encourages good practices, but proper implementation is essential.

Related FAQs

How do I handle spam Webmentions?

Strategies and techniques for preventing, detecting, and moderating spam Webmentions on your website.

How do I implement Webmentions on my site?

Step-by-step guide to adding Webmention support to your website, from beginner to advanced.

What are microformats and why do they matter for Webmentions?

Learn how microformats add semantic meaning to your content and enable rich Webmention interactions.

By

Commune Software