agentsclimarketplace

Linktree security basics

Skill jeremylongshore/claude-code-plugins-plus-skills/plugins/saas-packs/linktree-pack/skills/linktree-security-basics

425 plugins, 2,810 skills, 200 agents for Claude Code. Open-source marketplace at tonsofskills.com with the ccpi CLI package manager.

Install
npx -y skills add jeremylongshore/claude-code-plugins-plus-skills --skill linktree-security-basics

Assembled from the repository path, not quoted from the project. Check it against their README if it does not work.

What its author says it does

Copied from the file, not written here

'Security Basics for Linktree.

The file declares its own license as MIT. That is the author’s claim about this one file, and it is not the same thing as the license GitHub reports for the repository, which is listed with the other numbers below.

SKILL.md

5.0 KB, as published. Nobody here has run it

Linktree Security Basics

Overview

Linktree integrations handle user-generated content (link titles, URLs, bios) and analytics data that is PII-adjacent — click counts, geographic breakdowns, and referrer URLs can fingerprint individual visitors. Bearer token authentication means a leaked key grants full account access including link creation, profile modification, and analytics export. Webhook payloads carry real-time event data signed with HMAC-SHA256, and failing to verify signatures opens your endpoint to spoofed events and data poisoning.

Prerequisites

  • Secrets manager (AWS SSM, GCP Secret Manager, or Vault) for all Linktree credentials
  • HTTPS enforced on all webhook receiver endpoints
  • .env files in .gitignore — never committed to version control
  • Logging infrastructure that supports field-level redaction

API Key Management

// Load Linktree bearer token from environment — never hardcode
const LINKTREE_TOKEN = process.env.LINKTREE_API_KEY;

function validateLinktreeConfig(): void {
  if (!LINKTREE_TOKEN || LINKTREE_TOKEN.startsWith('lt_test_')) {
    throw new Error('Missing or test-only LINKTREE_API_KEY — set a production token');
  }
}

function linktreeHeaders(): Record<string, string> {
  return {
    Authorization: `Bearer ${LINKTREE_TOKEN}`,
    'Content-Type': 'application/json',
  };
}
// Call validateLinktreeConfig() at startup, before accepting requests

Webhook Signature Verification

import crypto from 'node:crypto';

const WEBHOOK_SECRET = process.env.LINKTREE_WEBHOOK_SECRET!;

function verifyLinktreeWebhook(payload: string, signature: string): boolean {
  const expected = crypto
    .createHmac('sha256', WEBHOOK_SECRET)
    .update(payload, 'utf8')
    .digest('hex');
  // Timing-safe comparison prevents timing attacks
  return crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected));
}

// Express middleware
app.post('/webhooks/linktree', (req, res) => {
  const sig = req.headers['x-linktree-signature'] as string;
  if (!sig || !verifyLinktreeWebhook(JSON.stringify(req.body), sig)) {
    return res.status(401).json({ error: 'Invalid signature' });
  }
  // Process verified event
});

Input Validation

import { URL } from 'node:url';

function sanitizeLinkTitle(title: string): string {
  // Strip HTML/script tags from user-generated link titles
  return title.replace(/<[^>]*>/g, '').trim().slice(0, 150);
}

function validateLinkUrl(url: string): boolean {
  try {
    const parsed = new URL(url);
    // Only allow http/https — block javascript:, data:, file: schemes
    return ['http:', 'https:'].includes(parsed.protocol);
  } catch {
    return false;
  }
}

Data Protection

function redactAnalytics(data: Record<string, unknown>): Record<string, unknown> {
  const sensitive = ['ip_address', 'user_agent', 'referrer_url', 'geo_city'];
  const redacted = { ...data };
  for (const field of sensitive) {
    if (redacted[field]) redacted[field] = '[REDACTED]';
  }
  return redacted;
}
// Use redactAnalytics() before writing any analytics payload to logs

Access Control

// Linktree tokens are account-scoped — enforce least privilege
function assertReadOnlyScope(operation: string): void {
  const writeOps = ['create_link', 'update_link', 'delete_link', 'update_profile'];
  if (writeOps.includes(operation) && process.env.LINKTREE_READ_ONLY === 'true') {
    throw new Error(`Write operation "${operation}" blocked in read-only mode`);
  }
}

Security Checklist

  • Bearer token stored in secrets manager, not .env on disk
  • Webhook x-linktree-signature verified with HMAC-SHA256
  • Link URLs validated against allowlisted protocols
  • Link titles sanitized for HTML/XSS before storage or display
  • Analytics data redacted before logging (IP, user-agent, geo)
  • Read-only mode enforced for non-admin integrations
  • Token rotation scheduled quarterly
  • Rate limiting applied to webhook receiver endpoint

Error Handling

VulnerabilityRiskMitigation
Bearer token in logsFull account takeoverRedact Authorization header in all log output
Unverified webhooksSpoofed link-click eventsReject any request missing valid x-linktree-signature
Malicious link URLsOpen redirect / phishingValidate URL scheme and domain before storing
XSS in link titlesScript injection via UGCStrip HTML tags and enforce max length
Analytics PII leakageGDPR/CCPA violationRedact IP, geo, and referrer before persistence

Resources

Next Steps

See linktree-prod-checklist.

Keep looking

Skills are one crate of 328,083. Ordering is by how many stacks a row turns up in, so the top of any crate is what has actually been picked rather than what has the most stars.