CVE-2014-10065
Description
A bypass in remarkable before 1.4.1 allows injection of javascript: URLs into rendered content via crafted input that evades the bad protocol check.
AI Insight
LLM-synthesized narrative grounded in this CVE's description and references.
A bypass in remarkable before 1.4.1 allows injection of javascript: URLs into rendered content via crafted input that evades the bad protocol check.
Vulnerability
In remarkable, a Markdown parsing library for JavaScript, versions prior to 1.4.1 are vulnerable to a URL injection flaw. The library includes a validation function intended to disallow dangerous schemes like javascript:. However, certain crafted inputs can bypass this check, allowing javascript: URLs to be injected into the final rendered HTML. The issue was addressed in commit d54ed887f4997221cd7cb9790e953a83c504de36 [2], which normalizes links before validation. The official description confirms that versions before 1.4.1 are affected [1].
Exploitation
An attacker who can supply user-controlled Markdown content (e.g., in a comment, forum post, or any application rendering Markdown via remarkable) can craft input that passes the protocol validation but eventually results in a javascript: URL when rendered. The attack requires no special network position and does not require authentication beyond normal content submission permissions. The fix introduced a normalizeLink function that first decodes and re-encodes the URL before validation, preventing the bypass [2].
Impact
Successful exploitation allows an attacker to inject arbitrary JavaScript into a web page that renders the Markdown. This could lead to cross-site scripting (XSS) attacks, where the attacker executes scripts in the context of the victim's browser, potentially stealing cookies, session tokens, or performing actions on behalf of the user. The impact is based on the privileges and context of the application using remarkable.
Mitigation
Upgrade to remarkable version 1.4.1 or later, which includes the normalization fix [2]. No workarounds are documented in the available references. The vulnerability was disclosed in 2014 and has been considered patched since version 1.4.1 [1].
AI Insight generated on May 22, 2026. Synthesized from this CVE's description and the cited reference URLs; citations are validated against the source bundle.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
remarkablenpm | < 1.4.1 | 1.4.1 |
Affected products
2- HackerOne/remarkable node modulev5Range: <1.4.1
Patches
1d54ed887f499Normalize links before they hit renderer
5 files changed · +44 −13
lib/helpers/normalize_link.js+9 −0 added@@ -0,0 +1,9 @@ +'use strict'; + + +var replaceEntities = require('../common/utils').replaceEntities; + + +module.exports = function normalizeLink(url) { + return encodeURI(decodeURI(replaceEntities(url))); +};
lib/helpers/parse_link_destination.js+9 −5 modified@@ -6,11 +6,12 @@ 'use strict'; -var unescapeMd = require('../common/utils').unescapeMd; +var normalizeLink = require('./normalize_link'); +var unescapeMd = require('../common/utils').unescapeMd; module.exports = function parseLinkDestination(state, pos) { - var code, level, + var code, level, link, start = pos, max = state.posMax; @@ -20,8 +21,10 @@ module.exports = function parseLinkDestination(state, pos) { code = state.src.charCodeAt(pos); if (code === 0x0A /* \n */) { return false; } if (code === 0x3E /* > */) { + link = normalizeLink(unescapeMd(state.src.slice(start + 1, pos))); + if (!state.parser.validateLink(link)) { return false; } state.pos = pos + 1; - state.linkContent = unescapeMd(state.src.slice(start + 1, pos)); + state.linkContent = link; return true; } if (code === 0x5C /* \ */ && pos + 1 < max) { @@ -67,9 +70,10 @@ module.exports = function parseLinkDestination(state, pos) { if (start === pos) { return false; } - state.linkContent = unescapeMd(state.src.slice(start, pos)); - if (!state.parser.validateLink(state.linkContent)) { return false; } + link = normalizeLink(unescapeMd(state.src.slice(start, pos))); + if (!state.parser.validateLink(link)) { return false; } + state.linkContent = link; state.pos = pos; return true; };
lib/renderer.js+2 −2 modified@@ -147,15 +147,15 @@ rules.paragraph_close = function (tokens, idx /*, options, env */) { rules.link_open = function (tokens, idx /*, options, env */) { var title = tokens[idx].title ? (' title="' + escapeHtml(replaceEntities(tokens[idx].title)) + '"') : ''; - return '<a href="' + escapeHtml(encodeURI(decodeURI(replaceEntities(tokens[idx].href)))) + '"' + title + '>'; + return '<a href="' + escapeHtml(tokens[idx].href) + '"' + title + '>'; }; rules.link_close = function (/* tokens, idx, options, env */) { return '</a>'; }; rules.image = function (tokens, idx, options /*, env */) { - var src = ' src="' + escapeHtml(encodeURI(tokens[idx].src)) + '"'; + var src = ' src="' + escapeHtml(tokens[idx].src) + '"'; var title = tokens[idx].title ? (' title="' + escapeHtml(replaceEntities(tokens[idx].title)) + '"') : ''; var alt = ' alt="' + (tokens[idx].alt ? escapeHtml(replaceEntities(tokens[idx].alt)) : '') + '"'; var suffix = options.xhtmlOut ? ' /' : '';
lib/rules_inline/autolink.js+8 −6 modified@@ -2,7 +2,8 @@ 'use strict'; -var url_schemas = require('../common/url_schemas'); +var url_schemas = require('../common/url_schemas'); +var normalizeLink = require('../helpers/normalize_link'); /*eslint max-len:0*/ @@ -11,7 +12,7 @@ var AUTOLINK_RE = /^<([a-zA-Z.\-]{1,25}):([^<>\x00-\x20]*)>/; module.exports = function autolink(state, silent) { - var tail, linkMatch, emailMatch, url, pos = state.pos; + var tail, linkMatch, emailMatch, url, fullUrl, pos = state.pos; if (state.src.charCodeAt(pos) !== 0x3C/* < */) { return false; } @@ -25,13 +26,13 @@ module.exports = function autolink(state, silent) { if (url_schemas.indexOf(linkMatch[1].toLowerCase()) < 0) { return false; } url = linkMatch[0].slice(1, -1); - + fullUrl = normalizeLink(url); if (!state.parser.validateLink(url)) { return false; } if (!silent) { state.push({ type: 'link_open', - href: url, + href: fullUrl, level: state.level }); state.push({ @@ -52,12 +53,13 @@ module.exports = function autolink(state, silent) { url = emailMatch[0].slice(1, -1); - if (!state.parser.validateLink('mailto:' + url)) { return false; } + fullUrl = normalizeLink('mailto:' + url); + if (!state.parser.validateLink(fullUrl)) { return false; } if (!silent) { state.push({ type: 'link_open', - href: 'mailto:' + url, + href: fullUrl, level: state.level }); state.push({
test/fixtures/remarkable/commonmark_extras.txt+16 −0 modified@@ -107,3 +107,19 @@ a <p>a</p> <?php . + +Normalize link destination, but not text inside it: + +. +<http://example.com/α%CE%B2γ%CE%B4> +. +<p><a href="http://example.com/%CE%B1%CE%B2%CE%B3%CE%B4">http://example.com/α%CE%B2γ%CE%B4</a></p> +. + +Autolinks do not allow escaping: + +. +<http://example.com/\[\> +. +<p><a href="http://example.com/%5C%5B%5C">http://example.com/\[\</a></p> +.
Vulnerability mechanics
Generated on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
6- github.com/advisories/GHSA-f9vc-q3hh-qhfvghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2014-10065ghsaADVISORY
- github.com/jonschlinkert/remarkable/commit/d54ed887f4997221cd7cb9790e953a83c504de36ghsaWEB
- github.com/jonschlinkert/remarkable/issues/97ghsax_refsource_MISCWEB
- nodesecurity.io/advisories/30mitrex_refsource_MISC
- www.npmjs.com/advisories/30ghsaWEB
News mentions
0No linked articles in our index yet.