VYPR
Moderate severityNVD Advisory· Published May 22, 2018· Updated Aug 5, 2024

CVE-2018-11093

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.

PackageAffected versionsPatched versions
@ckeditor/ckeditor5-linknpm
>= 0.3.0, < 10.0.110.0.1

Affected products

1

Patches

1
8cb782eceba1

Merge branch 's/ckeditor5-link/1'

https://github.com/ckeditor/ckeditor5-linkPiotrek KoszulińskiMay 22, 2018via ghsa
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

News mentions

0

No linked articles in our index yet.