{"record":{"id":"ae3665b5f36f3f19","repo":"koala73/worldmonitor","slug":"webhook-url-must-not-point-to-a-metadata-endpoint","errorCode":null,"errorMessage":"Webhook URL must not point to a metadata endpoint","messagePattern":"Webhook URL must not point to a metadata endpoint","errorType":"validation","errorClass":"Error","httpStatus":400,"severity":"warning","filePath":"api/_notification-webhook-ssrf.ts","lineNumber":239,"sourceCode":"async function defaultResolveHostname(hostname: string): Promise<string[]> {\n  const records = await Promise.all([\n    resolveDnsJson(hostname, 'A'),\n    resolveDnsJson(hostname, 'AAAA'),\n  ]);\n  return records.flat();\n}\n\n/**\n * Fail fast at registration when the webhook hostname currently resolves to a\n * private or reserved address. Delivery repeats this check (and pins its\n * connection) because DNS can change after registration.\n */\nexport async function assertNotificationWebhookRegistrationUrlSafe(\n  rawUrl: string,\n  resolveHostname: ResolveHostname = defaultResolveHostname,\n): Promise<void> {\n  const staticError = blockedNotificationWebhookUrlReason(rawUrl);\n  if (staticError) throw new Error(staticError);\n\n  const hostname = new URL(rawUrl).hostname.toLowerCase();\n  if (isIpLiteral(hostname)) return;\n  let resolvedAddresses: string[];\n  try {\n    resolvedAddresses = await resolveHostname(hostname);\n  } catch (error) {\n    const message = error instanceof Error ? error.message : String(error);\n    throw new Error(`Webhook URL DNS resolution failed: ${message}`);\n  }\n  if (!resolvedAddresses.length) throw new Error('Webhook URL DNS resolution returned no addresses');\n  if (resolvedAddresses.some(isBlockedNotificationResolvedAddress)) {\n    throw new Error('Webhook URL must not point to a private/local address');\n  }\n}\n","sourceCodeStart":221,"sourceCodeEnd":255,"githubUrl":"https://github.com/koala73/worldmonitor/blob/7d06c8633d256c18e38133030bc3613976a96ec9/api/_notification-webhook-ssrf.ts#L221-L255","documentation":"saveImportedFramework() enforces a cap of MAX_IMPORTED = 20 user-imported analysis frameworks stored in localStorage under the key 'wm-analysis-frameworks' (built-in frameworks are not counted). When the imported array already holds 20 entries, the save throws before any payload validation. The cap bounds localStorage growth of the analysis framework library.","triggerScenarios":"Calling saveImportedFramework(fw) when loadFromStorage('wm-analysis-frameworks') already returns 20 items, e.g., importing the 21st framework through the import UI or restoring a backup collection.","commonSituations":"Power users accumulating imports over time; scripts or tests importing in a loop; stale localStorage from an older version holding more entries than the visible list suggests.","solutions":["Delete one or more imported frameworks first via deleteImportedFramework(id) (built-ins cannot be deleted and do not count against the cap)","Verify the actual count: JSON.parse(localStorage.getItem('wm-analysis-frameworks')).length","Export and prune: keep only frameworks you actively use, then re-import selectively","If the visible list looks smaller than 20, clear the stale 'wm-analysis-frameworks' key and re-import what you need"],"exampleFix":"// before\nsaveImportedFramework(fw); // throws when 20 imports already stored\n\n// after\nconst importedCount = loadFrameworkLibrary().filter(f => !f.isBuiltIn).length;\nif (importedCount >= 20) {\n  promptUserToDeleteFirst();\n  return;\n}\nsaveImportedFramework(fw);","handlingStrategy":"validation","validationCode":"const importedCount = loadFrameworkLibrary().filter(f => !f.isBuiltIn).length;\nif (importedCount >= 20) { promptDeleteFirst(); return; }\nsaveImportedFramework(fw);","typeGuard":null,"tryCatchPattern":"try {\n  saveImportedFramework(fw);\n} catch (e) {\n  if (e instanceof Error && e.message.startsWith('Library is full')) offerFrameworkDeletionUI();\n  else throw e;\n}","preventionTips":["Show an import counter (n/20) in the library UI","Bulk imports should check remaining capacity before starting, not per-item mid-loop","Periodically prune unused imported frameworks"],"tags":["local-storage","quota","analysis-frameworks","validation"],"backgroundTag":"quota-limit-exceeded","analyzedSha":"7d06c8633d256c18e38133030bc3613976a96ec9","analyzedAt":"2026-08-21T16:51:25.751Z","contentChangedAt":"2026-08-21T16:51:25.751Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}