CVE-2018-11093
Description
CKEditor 5 Link package before 10.0.1 allows XSS via crafted href attribute in link elements.
AI Insight
LLM-synthesized narrative grounded in this CVE's description and references.
CKEditor 5 Link package before 10.0.1 allows XSS via crafted href attribute in link elements.
Vulnerability
A cross-site scripting (XSS) vulnerability exists in the Link package for CKEditor 5 versions prior to 10.0.1. The flaw allows remote attackers to inject arbitrary web script through a crafted href attribute of a link (``) element. The vulnerability was addressed in version 10.0.1, released on 2018-05-22 [1][2][3].
Exploitation
An attacker can exploit this vulnerability by providing a malicious href value (e.g., javascript:alert(1)) that is not properly sanitized by the editor. No authentication is required; the attack can be delivered via any content that includes a crafted link, such as a blog comment or a document. When a user interacts with the link (e.g., clicks it), the injected script executes in the context of the editor's domain [2][4].
Impact
Successful exploitation allows arbitrary JavaScript execution within the browser session of the user viewing the crafted content. This can lead to information disclosure, session hijacking, or other client-side attacks. The attacker gains the same privileges as the victim user within the editor's context [2].
Mitigation
Upgrade the Link package to version 10.0.1 or later, which includes input sanitization via the ensureSafeUrl() function that blocks dangerous URL schemes (e.g., javascript:) [1][3][4]. No workaround is available for earlier versions. The fix was released on 2018-05-22 [1][3].
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 |
|---|---|---|
@ckeditor/ckeditor5-linknpm | >= 0.3.0, < 10.0.1 | 10.0.1 |
Affected products
1Patches
18cb782eceba1Merge branch 's/ckeditor5-link/1'
6 files changed · +190 −7
src/linkediting.js+7 −2 modified@@ -14,7 +14,7 @@ import { import { upcastElementToAttribute } from '@ckeditor/ckeditor5-engine/src/conversion/upcast-converters'; import LinkCommand from './linkcommand'; import UnlinkCommand from './unlinkcommand'; -import { createLinkElement } from './utils'; +import { createLinkElement, ensureSafeUrl } from './utils'; import bindTwoStepCaretToAttribute from '@ckeditor/ckeditor5-engine/src/utils/bindtwostepcarettoattribute'; import findLinkRange from './findlinkrange'; import '../theme/link.css'; @@ -38,9 +38,14 @@ export default class LinkEditing extends Plugin { // Allow link attribute on all inline nodes. editor.model.schema.extend( '$text', { allowAttributes: 'linkHref' } ); - editor.conversion.for( 'downcast' ) + editor.conversion.for( 'dataDowncast' ) .add( downcastAttributeToElement( { model: 'linkHref', view: createLinkElement } ) ); + editor.conversion.for( 'editingDowncast' ) + .add( downcastAttributeToElement( { model: 'linkHref', view: ( href, writer ) => { + return createLinkElement( ensureSafeUrl( href ), writer ); + } } ) ); + editor.conversion.for( 'upcast' ) .add( upcastElementToAttribute( { view: {
src/ui/linkactionsview.js+3 −1 modified@@ -16,6 +16,8 @@ import FocusTracker from '@ckeditor/ckeditor5-utils/src/focustracker'; import FocusCycler from '@ckeditor/ckeditor5-ui/src/focuscycler'; import KeystrokeHandler from '@ckeditor/ckeditor5-utils/src/keystrokehandler'; +import { ensureSafeUrl } from '../utils'; + import unlinkIcon from '../../theme/icons/unlink.svg'; import pencilIcon from '@ckeditor/ckeditor5-core/theme/icons/pencil.svg'; import '../../theme/linkactions.css'; @@ -206,7 +208,7 @@ export default class LinkActionsView extends View { 'ck', 'ck-link-actions__preview' ], - href: bind.to( 'href' ), + href: bind.to( 'href', href => href && ensureSafeUrl( href ) ), target: '_blank' } } );
src/utils.js+29 −0 modified@@ -9,6 +9,9 @@ const linkElementSymbol = Symbol( 'linkElement' ); +const ATTRIBUTE_WHITESPACES = /[\u0000-\u0020\u00A0\u1680\u180E\u2000-\u2029\u205f\u3000]/g; // eslint-disable-line no-control-regex +const SAFE_URL = /^(?:(?:https?|ftps?|mailto):|[^a-z]|[a-z+.-]+(?:[^a-z+.:-]|$))/i; + /** * Returns `true` if a given view node is the link element. * @@ -32,3 +35,29 @@ export function createLinkElement( href, writer ) { return linkElement; } + +/** + * Returns a safe URL based on a given value. + * + * An URL is considered safe if it is safe for the user (does not contain any malicious code). + * + * If URL is considered unsafe, a simple `"#"` is returned. + * + * @protected + * @param {*} url + * @returns {String} Safe URL. + */ +export function ensureSafeUrl( url ) { + url = String( url ); + + return isSafeUrl( url ) ? url : '#'; +} + +// Checks whether the given URL is safe for the user (does not contain any malicious code). +// +// @param {String} url URL to check. +function isSafeUrl( url ) { + const normalizedUrl = url.replace( ATTRIBUTE_WHITESPACES, '' ); + + return normalizedUrl.match( SAFE_URL ); +}
tests/linkediting.js+26 −0 modified@@ -118,6 +118,25 @@ describe( 'LinkEditing', () => { expect( editor.getData() ).to.equal( '<p><a href="">foo</a>bar</p>' ); } ); + + // The editor's role is not to filter out potentially malicious data. + // Its job is to not let this code be executed inside the editor (see the test in "editing pipeline conversion"). + it( 'should output a link with a potential XSS code', () => { + setModelData( model, '<paragraph>[]</paragraph>' ); + + model.change( writer => { + writer.insertText( 'foo', { linkHref: 'javascript:alert(1)' }, model.document.selection.getFirstPosition() ); + } ); + + expect( editor.getData() ).to.equal( '<p><a href="javascript:alert(1)">foo</a></p>' ); + } ); + + it( 'should load a link with a potential XSS code', () => { + editor.setData( '<p><a href="javascript:alert(1)">foo</a></p>' ); + + expect( getModelData( model, { withoutSelection: true } ) ) + .to.equal( '<paragraph><$text linkHref="javascript:alert(1)">foo</$text></paragraph>' ); + } ); } ); describe( 'editing pipeline conversion', () => { @@ -147,6 +166,13 @@ describe( 'LinkEditing', () => { expect( editor.getData() ).to.equal( '<p><a href="url">a<f>b</f>c</a></p>' ); } ); + + it( 'must not render a link with a potential XSS code', () => { + setModelData( model, '<paragraph><$text linkHref="javascript:alert(1)">[]foo</$text>bar[]</paragraph>' ); + + expect( getViewData( editor.editing.view, { withoutSelection: true } ) ) + .to.equal( '<p><a href="#">foo</a>bar</p>' ); + } ); } ); describe( 'link highlighting', () => {
tests/ui/linkactionsview.js+7 −1 modified@@ -88,7 +88,7 @@ describe( 'LinkActionsView', () => { expect( view.previewButtonView.element.getAttribute( 'target' ) ).to.equal( '_blank' ); } ); - describe( '<button> bindings', () => { + describe( '<a> bindings', () => { it( 'binds href DOM attribute to view#href', () => { expect( view.previewButtonView.element.getAttribute( 'href' ) ).to.be.null; @@ -97,6 +97,12 @@ describe( 'LinkActionsView', () => { expect( view.previewButtonView.element.getAttribute( 'href' ) ).to.equal( 'foo' ); } ); + it( 'does not render unsafe view#href', () => { + view.href = 'javascript:alert(1)'; + + expect( view.previewButtonView.element.getAttribute( 'href' ) ).to.equal( '#' ); + } ); + it( 'binds #isEnabled to view#href', () => { expect( view.previewButtonView.isEnabled ).to.be.false;
tests/utils.js+118 −3 modified@@ -9,10 +9,10 @@ import AttributeElement from '@ckeditor/ckeditor5-engine/src/view/attributeeleme import ContainerElement from '@ckeditor/ckeditor5-engine/src/view/containerelement'; import Text from '@ckeditor/ckeditor5-engine/src/view/text'; -import { createLinkElement, isLinkElement } from '../src/utils'; +import { createLinkElement, isLinkElement, ensureSafeUrl } from '../src/utils'; describe( 'utils', () => { - describe( 'isLinkElement', () => { + describe( 'isLinkElement()', () => { it( 'should return true for elements created by createLinkElement', () => { const element = createLinkElement( 'http://ckeditor.com', new ViewWriter( new ViewDocument() ) ); @@ -32,7 +32,7 @@ describe( 'utils', () => { } ); } ); - describe( 'createLinkElement', () => { + describe( 'createLinkElement()', () => { it( 'should create link AttributeElement', () => { const element = createLinkElement( 'http://cksource.com', new ViewWriter( new ViewDocument() ) ); @@ -42,4 +42,119 @@ describe( 'utils', () => { expect( element.name ).to.equal( 'a' ); } ); } ); + + describe( 'ensureSafeUrl()', () => { + it( 'returns the same absolute http URL', () => { + const url = 'http://xx.yy/zz#foo'; + + expect( ensureSafeUrl( url ) ).to.equal( url ); + } ); + + it( 'returns the same absolute https URL', () => { + const url = 'https://xx.yy/zz'; + + expect( ensureSafeUrl( url ) ).to.equal( url ); + } ); + + it( 'returns the same absolute ftp URL', () => { + const url = 'ftp://xx.yy/zz'; + + expect( ensureSafeUrl( url ) ).to.equal( url ); + } ); + + it( 'returns the same absolute ftps URL', () => { + const url = 'ftps://xx.yy/zz'; + + expect( ensureSafeUrl( url ) ).to.equal( url ); + } ); + + it( 'returns the same absolute mailto URL', () => { + const url = 'mailto://foo@bar.com'; + + expect( ensureSafeUrl( url ) ).to.equal( url ); + } ); + + it( 'returns the same relative URL (starting with a dot)', () => { + const url = './xx/yyy'; + + expect( ensureSafeUrl( url ) ).to.equal( url ); + } ); + + it( 'returns the same relative URL (starting with two dots)', () => { + const url = '../../xx/yyy'; + + expect( ensureSafeUrl( url ) ).to.equal( url ); + } ); + + it( 'returns the same relative URL (starting with a slash)', () => { + const url = '/xx/yyy'; + + expect( ensureSafeUrl( url ) ).to.equal( url ); + } ); + + it( 'returns the same relative URL (starting with a backslash)', () => { + const url = '\\xx\\yyy'; + + expect( ensureSafeUrl( url ) ).to.equal( url ); + } ); + + it( 'returns the same relative URL (starting with a letter)', () => { + const url = 'xx/yyy'; + + expect( ensureSafeUrl( url ) ).to.equal( url ); + } ); + + it( 'returns the same URL even if it contains whitespaces', () => { + const url = ' ./xx/ yyy\t'; + + expect( ensureSafeUrl( url ) ).to.equal( url ); + } ); + + it( 'returns the same URL even if it contains non ASCII characters', () => { + const url = 'https://kłącze.yy/źdźbło'; + + expect( ensureSafeUrl( url ) ).to.equal( url ); + } ); + + it( 'accepts non string values', () => { + expect( ensureSafeUrl( undefined ) ).to.equal( 'undefined' ); + expect( ensureSafeUrl( null ) ).to.equal( 'null' ); + } ); + + it( 'returns safe URL when a malicious URL starts with javascript:', () => { + const url = 'javascript:alert(1)'; + + expect( ensureSafeUrl( url ) ).to.equal( '#' ); + } ); + + it( 'returns safe URL when a malicious URL starts with an unknown protocol', () => { + const url = 'foo:alert(1)'; + + expect( ensureSafeUrl( url ) ).to.equal( '#' ); + } ); + + it( 'returns safe URL when a malicious URL contains spaces', () => { + const url = 'java\u0000script:\talert(1)'; + + expect( ensureSafeUrl( url ) ).to.equal( '#' ); + } ); + + it( 'returns safe URL when a malicious URL contains spaces (2)', () => { + const url = '\u0000 javascript:alert(1)'; + + expect( ensureSafeUrl( url ) ).to.equal( '#' ); + } ); + + it( 'returns safe URL when a malicious URL contains a safe part', () => { + const url = 'javascript:alert(1)\nhttp://xxx'; + + expect( ensureSafeUrl( url ) ).to.equal( '#' ); + } ); + + it( 'returns safe URL when a malicious URL contains a safe part (2)', () => { + const url = 'javascript:alert(1);http://xxx'; + + expect( ensureSafeUrl( url ) ).to.equal( '#' ); + } ); + } ); } );
Vulnerability mechanics
Generated on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
8- github.com/advisories/GHSA-gvpx-9459-w3mjghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2018-11093ghsaADVISORY
- ckeditor.com/blog/CKEditor-5-v10.0.1-releasedghsaWEB
- ckeditor.com/blog/CKEditor-5-v10.0.1-released/mitrex_refsource_CONFIRM
- github.com/ckeditor/ckeditor5-link/blob/master/CHANGELOG.mdghsax_refsource_CONFIRMWEB
- github.com/ckeditor/ckeditor5-link/commit/8cb782eceba10fc481e4021cb5d25b2a85d1b04eghsaWEB
- snyk.io/vuln/SNYK-JS-CKEDITORCKEDITOR5LINK-72892ghsaWEB
- www.npmjs.com/advisories/1154ghsaWEB
News mentions
0No linked articles in our index yet.