{"id":"CVE-2026-113661","kind":"vulnerability","aliases":["CVE-2026-113661","GHSA-cv27-6wvh-8x7j"],"related":[],"title":"ApostropheCMS: Incomplete CVE-2026-53606: `meta http-equiv=refresh` `content` still ships `javascript:` / `data:` URLs","description":"### Summary\n\nCVE-2026-53606 expanded `allowedSchemesAppliedToAttributes` so named URL attributes (`action`, `formaction`, `data`, `poster`, `background`, later `ping`, `xlink:href`, …) go through `naughtyHref()`. A **URL nested inside `meta` `content`** (`http-equiv=refresh`) is not an attribute name on that list, so the embedded destination is never scheme-checked.\n\nDefault `allowedTags` still omit `meta`. Same non-default integrator config as 53606: allow `meta` plus `http-equiv` / `content`.\n\nOn **2.17.7**:\n\n```text\nsanitizeHtml(\n  '<meta http-equiv=\"refresh\" content=\"0;url=javascript:alert(1)\">',\n  { allowedTags: ['meta'], allowedAttributes: { meta: ['http-equiv', 'content'] } }\n)\n// => <meta http-equiv=\"refresh\" content=\"0;url=javascript:alert(1)\" />\n```\n\n`data:text/html,…` and arbitrary `https:` destinations also survive. `formaction=\"javascript:…\"` (53606) is stripped.\n\nPlease **Request CVE**. Credit: Finder + Reporter (`adrbogacz`).\n\n### Details\n\n`naughtyHref()` only runs when the attribute name is in `allowedSchemesAppliedToAttributes` (`index.js` ~385). Refresh URLs live in a `content` value of the form `0;url=…`, so the gate never fires. Adding `content` to that list wholesale would break ordinary meta `content` (description, viewport).\n\n**Fix:** when `http-equiv` is `refresh` (case-insensitive), parse the `; url=` destination and run `naughtyHref('meta', url)`. Drop `content` (or the tag) if the URL is rejected. Leave other `meta` `content` values alone.\n\n### PoC\n\nLocal `sanitize-html@2.17.7`. Output inspection only:\n\n```text\nnode -e '\nconst sanitize = require(\"sanitize-html\");\nconst opts = { allowedTags: [\"meta\"], allowedAttributes: { meta: [\"http-equiv\", \"content\"] } };\nfor (const html of [\n  \"<meta http-equiv=\\\"refresh\\\" content=\\\"0;url=javascript:alert(1)\\\">\",\n  \"<meta http-equiv=\\\"refresh\\\" content=\\\"0;url=data:text/html,aaa\\\">\",\n  \"<form><button formaction=\\\"javascript:alert(1)\\\">x</button></form>\",\n]) {\n  console.log(html, \"=>\", sanitize(html, html.includes(\"meta\") ? opts : { allowedTags: [\"form\",\"button\"], allowedAttributes: { button: [\"formaction\"] } }));\n}\n'\n```\n\nExpected: refresh `javascript:` / `data:` remain; 53606 `formaction` is removed. Do not attach a browser hang or stored-XSS page.\n\n### Impact\n\nAn integrator who allowed `meta` + `content` expecting `allowedSchemes` to apply (the 53606 configuration pattern) still emits a refresh to `javascript:`, `data:`, or an attacker `https:` URL. Stored XSS / open redirect for viewers of that HTML. Default config is not affected.","summary":"sanitize-html versions before 2.18.0 keep a meta refresh URL inside the content attribute even when its scheme is javascript: or data:, when an integrator has allowed meta with http-equiv and content. A viewer of the sanitized HTML can be redirected to attacker-chosen URLs or run script, which is stored XSS or open redirect.","severity":"medium","cvss":5.400000095367432,"epss":0.002630000002682209,"exploited":false,"exploited_at":null,"products":[{"name":"sanitize-html","fixed":["2.18.0"],"affected":"< 2.18.0","ecosystem":"npm"}],"ecosystems":["npm"],"tags":["cms","cwe-79","node","npm","package","sanitize-html","user-generated-html"],"not_affected_if":["default-config","meta-not-allowed","sanitize-html-2.18.0-or-later"],"how_to_check":"Run: npm ls sanitize-html\nSearch your sanitize-html calls for allowedTags or allowedAttributes containing 'meta' together with 'http-equiv' or 'content'.","fix":"Upgrade sanitize-html to 2.18.0 or later.\nIf you cannot upgrade yet, remove 'meta' from allowedTags or remove 'content' from the meta allowedAttributes.","status":"open","published_at":"2026-10-20T18:15:33+00:00","first_seen_at":"2026-10-23T19:11:32+00:00","triaged_at":"2026-10-23T19:58:18+00:00","updated_at":"2026-10-23T19:58:18+00:00","category":"package","confidence":0.8,"triage":{"at":"2026-10-23T19:58:18Z","model":"claude-haiku-5-5","output":{"fix":"Upgrade sanitize-html to 2.18.0 or later.\nIf you cannot upgrade yet, remove 'meta' from allowedTags or remove 'content' from the meta allowedAttributes.","summary":"sanitize-html versions before 2.18.0 keep a meta refresh URL inside the content attribute even when its scheme is javascript: or data:, when an integrator has allowed meta with http-equiv and content. A viewer of the sanitized HTML can be redirected to attacker-chosen URLs or run script, which is stored XSS or open redirect.","category":"package","who_cares":["npm","sanitize-html","node","cms","user-generated-html"],"confidence":0.8,"analyst_note":"","how_to_check":"Run: npm ls sanitize-html\nSearch your sanitize-html calls for allowedTags or allowedAttributes containing 'meta' together with 'http-equiv' or 'content'.\nunknown","needs_analyst":false,"not_affected_if":["default-config","meta-not-allowed","sanitize-html-2.18.0-or-later"],"severity_if_unknown":"medium"},"cost_usd":0.000503,"input_tokens":3085,"output_tokens":389,"severity_before":"medium"},"analyst_status":null,"analyst_note":null,"search":"'-113661':19A,27A '-2026':4A,18A,26A,105C '-53606':5A,106C '-79':33B '0':227C,320C,331C '1':324C,340C '2.17.7':173C,287C '2.18.0':49B '385':216C '53606':164C,194C,360C,387C '6wvh':23A '8x7j':24A 'aaa':335C 'action':113C 'add':234C 'adrbogacz':203C 'affect':415C 'alert':323C,339C 'allow':71B,165C,379C 'allowedattribut':178C,304C,352C 'allowedschem':383C 'allowedschemesappliedtoattribut':108C,214C 'allowedtag':153C,176C,302C,349C 'alon':281C 'also':190C 'apostrophecm':1A 'appli':385C 'arbitrari':187C 'attach':366C 'attack':90B,399C 'attacker-chosen':89B 'attribut':58B,112C,138C,210C 'background':117C 'break':241C 'browser':368C 'button':336C,351C,353C 'case':255C 'case-insensit':254C 'check':151C 'chosen':91B 'cms':31B 'config':162C,412C 'configur':388C 'console.log':342C 'const':294C,300C,311C 'content':11A,57B,78B,130C,171C,183C,222C,235C,244C,267C,279C,309C,319C,330C,381C 'credit':200C 'cv27':22A 'cve':3A,17A,25A,104C,199C 'cwe':32B 'data':15A,66B,115C,184C,333C,358C,396C 'default':152C,160C,411C 'descript':245C 'destin':146C,189C,260C 'detail':204C 'drop':266C 'e':293C 'embed':145C 'emit':391C 'equiv':9A,76B,133C,170C,182C,251C,308C,317C,328C 'even':59B 'expand':107C 'expect':355C,382C 'finder':201C 'fire':233C 'fix':247C 'form':226C,350C 'formact':114C,192C,337C,354C,361C 'gate':231C 'generat':42B 'ghsa':21A 'ghsa-cv27-6wvh-8x7j':20A 'go':122C 'hang':369C 'href':121C 'html':30A,39B,43B,46B,84B,286C,299C,312C,343C,345C,410C 'html.includes':346C 'http':8A,75B,132C,169C,181C,250C,307C,316C,327C 'http-equiv':7A,74B,131C,168C,180C,249C,306C,315C,326C 'https':188C,400C 'impact':375C 'incomplet':2A 'index.js':215C 'insensit':256C 'insid':55B,128C 'inspect':289C 'integr':69B,161C,377C 'javascript':14A,64B,193C,322C,338C,357C,395C 'keep':50B 'later':118C 'leav':276C 'list':142C,238C 'live':219C 'local':283C 'meta':6A,52B,72B,129C,156C,166C,177C,179C,243C,264C,278C,303C,305C,314C,325C,347C,380C 'name':110C,139C,211C 'naughtyhref':124C,205C,263C 'nest':127C 'never':148C,232C 'node':34B,292C 'non':159C 'non-default':158C 'npm':35B 'omit':155C 'open':101B,404C 'opt':301C,348C 'ordinari':242C 'output':288C 'packag':36B 'page':374C 'pars':257C 'pattern':389C 'ping':119C 'pleas':197C 'plus':167C 'poc':282C 'poster':116C 'redirect':87B,102B,405C 'refresh':10A,53B,134C,217C,253C,318C,329C,356C,393C 'reject':275C 'remain':359C 'remov':363C 'report':202C 'request':198C 'requir':296C 'run':94B,207C,262C 'sanit':38B,45B,83B,285C,295C,298C,344C 'sanitize':29A 'sanitize-html':28A,37B,44B,284C,297C 'sanitizehtml':175C 'scheme':62B,150C 'scheme-check':149C 'script':95B 'ship':13A 'still':12A,154C,390C 'store':98B,372C,402C 'stored-xss':371C 'strip':196C 'summari':103C 'surviv':191C 'tag':270C 'text':174C,291C 'text/html':185C,334C 'url':16A,54B,92B,111C,126C,218C,228C,259C,265C,273C,321C,332C,401C 'user':41B 'user-generated-html':40B 'valu':223C,280C 'version':47B 'viewer':80B,407C 'viewport':246C 'wholesal':239C 'would':240C 'x':341C 'xlink':120C 'xss':99B,373C,403C","nicknames":[],"mitigations":[],"who_affected":null,"tier":"C","verified":{},"analyst_revisit_at":null,"first_public_at":"2026-10-20T18:15:33+00:00","kev":null,"cvss_vector":"CVSS:3.1/AV:N/AC:L/PR:L/UI:R/S:C/C:L/I:L/A:N","cvss_by":"GitHub","poc_count":0,"sources":[{"source":"ghsa","external_id":"GHSA-cv27-6wvh-8x7j","url":"https://github.com/advisories/GHSA-cv27-6wvh-8x7j","severity":"medium","published_at":"2026-10-20T18:15:33+00:00","modified_at":"2026-10-20T18:15:34+00:00","first_seen_at":"2026-10-23T19:11:32+00:00","fetched_at":"2026-10-23T19:11:32+00:00","note":null},{"source":"nvd","external_id":"CVE-2026-113661","url":"https://nvd.nist.gov/vuln/detail/CVE-2026-113661","severity":null,"published_at":"2026-10-20T18:17:19+00:00","modified_at":"2026-10-21T16:17:24+00:00","first_seen_at":"2026-10-23T21:57:21+00:00","fetched_at":"2026-10-23T22:28:58+00:00","note":null}],"documents":[],"notice":"Free to read at a human pace. An API with keys and an MCP server are in preparation; early access: hello@infrastack.ai"}