All files / lib sanitize.ts

0% Statements 0/38
0% Branches 0/28
0% Functions 0/9
0% Lines 0/37

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156                                                                                                                                                                                                                                                                                                                       
import DOMPurify from "isomorphic-dompurify";
 
/**
 * HTML sanitization utilities
 * XSS攻撃を防ぐためのHTMLサニタイズ機能
 */
 
/**
 * Allowed HTML tags for post content
 */
const ALLOWED_TAGS_RICH = [
  "p", "br", "strong", "em", "u", "s",
  "h1", "h2", "h3", "h4", "h5", "h6",
  "ul", "ol", "li",
  "blockquote", "pre", "code",
  "a",
];
 
/**
 * Allowed HTML attributes
 */
const ALLOWED_ATTR = ["href", "title", "target"];
 
/**
 * Configuration for DOMPurify
 */
const DEFAULT_CONFIG = {
  ALLOWED_TAGS: ALLOWED_TAGS_RICH,
  ALLOWED_ATTR: ALLOWED_ATTR,
  ALLOW_DATA_ATTR: false,
  ALLOW_UNKNOWN_PROTOCOLS: false,
};
 
/**
 * Sanitize HTML content for rich text (posts, comments)
 * リッチテキスト用のHTMLサニタイズ(投稿本文など)
 *
 * @param dirty - Unsanitized HTML string
 * @returns Sanitized HTML string
 */
export function sanitizeHTML(dirty: string): string {
  if (!dirty || typeof dirty !== "string") {
    return "";
  }
 
  return DOMPurify.sanitize(dirty, DEFAULT_CONFIG);
}
 
/**
 * Sanitize plain text content (titles, summaries, usernames)
 * プレーンテキスト用のサニタイズ(タイトル、要約、ユーザー名など)
 *
 * @param dirty - Unsanitized text
 * @returns Sanitized plain text (all HTML tags removed)
 */
export function sanitizePlainText(dirty: string): string {
  if (!dirty || typeof dirty !== "string") {
    return "";
  }
 
  // Remove all HTML tags
  return DOMPurify.sanitize(dirty, {
    ALLOWED_TAGS: [],
    ALLOWED_ATTR: [],
  });
}
 
/**
 * Sanitize URL for safe use in links
 * URLを安全にサニタイズ
 *
 * @param url - URL to sanitize
 * @returns Sanitized URL or empty string if invalid
 */
export function sanitizeURL(url: string): string {
  if (!url || typeof url !== "string") {
    return "";
  }
 
  // Only allow http, https, and mailto protocols
  const allowedProtocols = ["http:", "https:", "mailto:"];
 
  try {
    const parsed = new URL(url);
    if (allowedProtocols.includes(parsed.protocol)) {
      return url;
    }
  } catch {
    // Invalid URL
    return "";
  }
 
  return "";
}
 
/**
 * Sanitize comment content
 * コメントコンテンツのサニタイズ
 *
 * @param dirty - Unsanitized comment text
 * @returns Sanitized comment (limited HTML allowed)
 */
export function sanitizeComment(dirty: string): string {
  if (!dirty || typeof dirty !== "string") {
    return "";
  }
 
  // Comments allow only basic formatting
  return DOMPurify.sanitize(dirty, {
    ALLOWED_TAGS: ["p", "br", "strong", "em", "a"],
    ALLOWED_ATTR: ["href"],
  });
}
 
/**
 * Escape HTML entities for safe display
 * HTMLエンティティをエスケープして安全に表示
 *
 * @param text - Text to escape
 * @returns Escaped text
 */
export function escapeHTML(text: string): string {
  if (!text || typeof text !== "string") {
    return "";
  }
 
  const map: Record<string, string> = {
    "&": "&amp;",
    "<": "&lt;",
    ">": "&gt;",
    '"': "&quot;",
    "'": "&#x27;",
    "/": "&#x2F;",
  };
 
  return text.replace(/[&<>"'/]/g, (char) => map[char] || char);
}
 
/**
 * Validate and sanitize tag input
 * タグ入力の検証とサニタイズ
 *
 * @param tags - Array of tag strings
 * @returns Array of sanitized tags (max 10 tags, max 20 chars each)
 */
export function sanitizeTags(tags: string[]): string[] {
  if (!Array.isArray(tags)) {
    return [];
  }
 
  return tags
    .map((tag) => sanitizePlainText(tag.trim()))
    .filter((tag) => tag.length > 0 && tag.length <= 20)
    .slice(0, 10); // Max 10 tags
}