@@ -20,6 +20,43 @@ const DEFAULT_CONFIG = {
2020 theme : 'light'
2121} ;
2222
23+ /**
24+ * Sanitize a URL to prevent XSS via javascript: or data: protocols
25+ *
26+ * @param {string } url - The URL to sanitize
27+ * @returns {string } Sanitized URL or empty string if invalid
28+ */
29+ function sanitizeUrl ( url ) {
30+ if ( typeof url !== 'string' ) {
31+ return '' ;
32+ }
33+
34+ const trimmedUrl = url . trim ( ) . toLowerCase ( ) ;
35+
36+ // Block dangerous protocols
37+ if ( trimmedUrl . startsWith ( 'javascript:' ) ||
38+ trimmedUrl . startsWith ( 'data:' ) ||
39+ trimmedUrl . startsWith ( 'vbscript:' ) ) {
40+ return '' ;
41+ }
42+
43+ // Only allow http, https, and relative URLs
44+ if ( trimmedUrl . startsWith ( 'http://' ) ||
45+ trimmedUrl . startsWith ( 'https://' ) ||
46+ trimmedUrl . startsWith ( '/' ) ||
47+ trimmedUrl . startsWith ( './' ) ||
48+ trimmedUrl . startsWith ( '../' ) ) {
49+ return url ;
50+ }
51+
52+ // For other cases, prepend https:// if it looks like a domain
53+ if ( trimmedUrl . match ( / ^ [ a - z 0 - 9 ] ( [ a - z 0 - 9 - ] * [ a - z 0 - 9 ] ) ? ( \. [ a - z ] { 2 , } ) + / ) ) {
54+ return 'https://' + url ;
55+ }
56+
57+ return '' ;
58+ }
59+
2360/**
2461 * Extract license metadata from the current page
2562 *
@@ -147,7 +184,8 @@ export function createLicenseBadge(options = {}) {
147184 const config = { ...DEFAULT_CONFIG , ...options } ;
148185 const badge = document . createElement ( 'a' ) ;
149186
150- badge . href = config . licenseUrl ;
187+ // Sanitize URL to prevent XSS via javascript: protocol
188+ badge . href = sanitizeUrl ( config . licenseUrl ) ;
151189 badge . target = '_blank' ;
152190 badge . rel = 'license noopener noreferrer' ;
153191 badge . className = 'palimpsest-badge' ;
@@ -182,14 +220,33 @@ export function createLicenseBadge(options = {}) {
182220 ? `Palimpsest Licentie v${ config . version } `
183221 : `Palimpsest License v${ config . version } ` ;
184222
185- badge . innerHTML = `
186- <svg width="16" height="16" viewBox="0 0 24 24" fill="none" style="margin-right: 6px;">
187- <path d="M12 2L2 7L12 12L22 7L12 2Z" stroke="currentColor" stroke-width="2"/>
188- <path d="M2 17L12 22L22 17" stroke="currentColor" stroke-width="2"/>
189- <path d="M2 12L12 17L22 12" stroke="currentColor" stroke-width="2"/>
190- </svg>
191- ${ text }
192- ` ;
223+ // Create SVG element safely
224+ const svg = document . createElementNS ( 'http://www.w3.org/2000/svg' , 'svg' ) ;
225+ svg . setAttribute ( 'width' , '16' ) ;
226+ svg . setAttribute ( 'height' , '16' ) ;
227+ svg . setAttribute ( 'viewBox' , '0 0 24 24' ) ;
228+ svg . setAttribute ( 'fill' , 'none' ) ;
229+ svg . style . marginRight = '6px' ;
230+
231+ const paths = [
232+ 'M12 2L2 7L12 12L22 7L12 2Z' ,
233+ 'M2 17L12 22L22 17' ,
234+ 'M2 12L12 17L22 12'
235+ ] ;
236+
237+ paths . forEach ( d => {
238+ const path = document . createElementNS ( 'http://www.w3.org/2000/svg' , 'path' ) ;
239+ path . setAttribute ( 'd' , d ) ;
240+ path . setAttribute ( 'stroke' , 'currentColor' ) ;
241+ path . setAttribute ( 'stroke-width' , '2' ) ;
242+ svg . appendChild ( path ) ;
243+ } ) ;
244+
245+ badge . appendChild ( svg ) ;
246+
247+ // Create text node safely (prevents XSS)
248+ const textNode = document . createTextNode ( text ) ;
249+ badge . appendChild ( textNode ) ;
193250
194251 badge . addEventListener ( 'mouseenter' , ( ) => {
195252 badge . style . borderColor = config . theme === 'dark' ? '#58a6ff' : '#0366d6' ;
@@ -240,29 +297,79 @@ export function createLicenseNotice(options = {}) {
240297 ` ;
241298
242299 const text = config . language === 'nl' ? {
243- main : `Dit werk is beschermd onder de <strong>Palimpsest Licentie v${ config . version } </strong>.` ,
300+ mainPrefix : 'Dit werk is beschermd onder de ' ,
301+ mainLicense : `Palimpsest Licentie v${ config . version } ` ,
302+ mainSuffix : '.' ,
244303 requirement : 'Afgeleiden moeten de emotionele en culturele integriteit van het origineel behouden.' ,
245304 link : 'Lees de volledige licentie'
246305 } : {
247- main : `This work is protected under the <strong>Palimpsest License v${ config . version } </strong>.` ,
306+ mainPrefix : 'This work is protected under the ' ,
307+ mainLicense : `Palimpsest License v${ config . version } ` ,
308+ mainSuffix : '.' ,
248309 requirement : "Derivatives must preserve the original's emotional and cultural integrity." ,
249310 link : 'Read the full license'
250311 } ;
251312
252- notice . innerHTML = `
253- <div style="display: flex; align-items: center;">
254- <svg width="24" height="24" viewBox="0 0 24 24" fill="none" style="margin-right: 12px; flex-shrink: 0;">
255- <path d="M12 2L2 7L12 12L22 7L12 2Z" stroke="${ styles . linkColor } " stroke-width="2"/>
256- <path d="M2 17L12 22L22 17" stroke="${ styles . linkColor } " stroke-width="2"/>
257- <path d="M2 12L12 17L22 12" stroke="${ styles . linkColor } " stroke-width="2"/>
258- </svg>
259- <div>
260- <p style="margin: 0 0 8px 0;">${ text . main } </p>
261- <p style="margin: 0 0 8px 0; font-size: 13px; opacity: 0.8;">${ text . requirement } </p>
262- <a href="${ config . licenseUrl } " style="color: ${ styles . linkColor } ; text-decoration: none; font-weight: 500;" target="_blank" rel="license noopener">${ text . link } →</a>
263- </div>
264- </div>
265- ` ;
313+ // Build DOM safely to prevent XSS
314+ const container = document . createElement ( 'div' ) ;
315+ container . style . cssText = 'display: flex; align-items: center;' ;
316+
317+ // Create SVG safely
318+ const svg = document . createElementNS ( 'http://www.w3.org/2000/svg' , 'svg' ) ;
319+ svg . setAttribute ( 'width' , '24' ) ;
320+ svg . setAttribute ( 'height' , '24' ) ;
321+ svg . setAttribute ( 'viewBox' , '0 0 24 24' ) ;
322+ svg . setAttribute ( 'fill' , 'none' ) ;
323+ svg . style . cssText = 'margin-right: 12px; flex-shrink: 0;' ;
324+
325+ const pathData = [
326+ 'M12 2L2 7L12 12L22 7L12 2Z' ,
327+ 'M2 17L12 22L22 17' ,
328+ 'M2 12L12 17L22 12'
329+ ] ;
330+
331+ pathData . forEach ( d => {
332+ const path = document . createElementNS ( 'http://www.w3.org/2000/svg' , 'path' ) ;
333+ path . setAttribute ( 'd' , d ) ;
334+ path . setAttribute ( 'stroke' , styles . linkColor ) ;
335+ path . setAttribute ( 'stroke-width' , '2' ) ;
336+ svg . appendChild ( path ) ;
337+ } ) ;
338+
339+ container . appendChild ( svg ) ;
340+
341+ // Create content div
342+ const contentDiv = document . createElement ( 'div' ) ;
343+
344+ // Main paragraph with safe text content
345+ const mainP = document . createElement ( 'p' ) ;
346+ mainP . style . cssText = 'margin: 0 0 8px 0;' ;
347+ mainP . appendChild ( document . createTextNode ( text . mainPrefix ) ) ;
348+ const strong = document . createElement ( 'strong' ) ;
349+ strong . textContent = text . mainLicense ;
350+ mainP . appendChild ( strong ) ;
351+ mainP . appendChild ( document . createTextNode ( text . mainSuffix ) ) ;
352+
353+ // Requirement paragraph
354+ const reqP = document . createElement ( 'p' ) ;
355+ reqP . style . cssText = 'margin: 0 0 8px 0; font-size: 13px; opacity: 0.8;' ;
356+ reqP . textContent = text . requirement ;
357+
358+ // Link with URL validation
359+ const link = document . createElement ( 'a' ) ;
360+ // Validate URL to prevent javascript: protocol injection
361+ const sanitizedUrl = sanitizeUrl ( config . licenseUrl ) ;
362+ link . href = sanitizedUrl ;
363+ link . style . cssText = `color: ${ styles . linkColor } ; text-decoration: none; font-weight: 500;` ;
364+ link . target = '_blank' ;
365+ link . rel = 'license noopener' ;
366+ link . textContent = text . link + ' →' ;
367+
368+ contentDiv . appendChild ( mainP ) ;
369+ contentDiv . appendChild ( reqP ) ;
370+ contentDiv . appendChild ( link ) ;
371+ container . appendChild ( contentDiv ) ;
372+ notice . appendChild ( container ) ;
266373
267374 return notice ;
268375}
0 commit comments