diff --git a/bits/79_html.js b/bits/79_html.js index e3b943d..1fd496d 100644 --- a/bits/79_html.js +++ b/bits/79_html.js @@ -93,7 +93,7 @@ function make_html_row(ws/*:Worksheet*/, r/*:Range*/, R/*:number*/, o/*:Sheet2HT if(cell.v != null) sp["data-v"] = escapehtml(cell.v instanceof Date ? cell.v.toISOString() : cell.v); if(cell.z != null) sp["data-z"] = cell.z; if(cell.f != null) sp["data-f"] = escapehtml(cell.f); - if(cell.l && (cell.l.Target || "#").charAt(0) != "#") w = '' + w + ''; + if(cell.l && (cell.l.Target || "#").charAt(0) != "#" && (!o.sanitizeLinks || (cell.l.Target || "").slice(0, 11).toLowerCase() != 'javascript:')) w = '' + w + ''; } sp.id = (o.id || "sjs") + "-" + coord; oo.push(writextag('td', w, sp)); diff --git a/test.js b/test.js index cbdb6bb..1ce1079 100644 --- a/test.js +++ b/test.js @@ -1529,6 +1529,21 @@ describe('write features', function() { var str = X.write(wb, {bookType:"html", type:"binary"}); assert.ok(str.indexOf("abc") > 0); }); + it('should remove javascript: URLs when sanitizeLinks is true', function() { + var sheet = X.utils.aoa_to_sheet([["Click me"]]); + get_cell(sheet, "A1").l = { Target: "javascript:alert('xss')" }; + + assert.ok(X.utils.sheet_to_html(sheet).indexOf("javascript:alert") > -1, "javascript: should not be stripped by default"); + + assert.ok(X.utils.sheet_to_html(sheet, { sanitizeLinks: true }).indexOf("javascript:alert") === -1, "javascript: should be stripped with sanitizeLinks"); + }); + it('should preserve non-javascript URLs with sanitizeLinks', function() { + var sheet = X.utils.aoa_to_sheet([["Link"]]); + get_cell(sheet, "A1").l = { Target: "https://example.com" }; + + var str = X.utils.sheet_to_html(sheet, { sanitizeLinks: true }); + assert.ok(str.indexOf('href="https://example.com"') > -1); + }); }); describe('sheet range limits', function() { [ ["biff2", "IV16384"], diff --git a/test.mjs b/test.mjs index 4a0cece..a7103fe 100644 --- a/test.mjs +++ b/test.mjs @@ -1533,6 +1533,21 @@ describe('write features', function() { var str = X.write(wb, {bookType:"html", type:"binary"}); assert.ok(str.indexOf("abc") > 0); }); + it('should remove javascript: URLs when sanitizeLinks is true', function() { + var sheet = X.utils.aoa_to_sheet([["Click me"]]); + get_cell(sheet, "A1").l = { Target: "javascript:alert('xss')" }; + + assert.ok(X.utils.sheet_to_html(sheet).indexOf("javascript:alert") > -1, "javascript: should not be stripped by default"); + + assert.ok(X.utils.sheet_to_html(sheet, { sanitizeLinks: true }).indexOf("javascript:alert") === -1, "javascript: should be stripped with sanitizeLinks"); + }); + it('should preserve non-javascript URLs with sanitizeLinks', function() { + var sheet = X.utils.aoa_to_sheet([["Link"]]); + get_cell(sheet, "A1").l = { Target: "https://example.com" }; + + var str = X.utils.sheet_to_html(sheet, { sanitizeLinks: true }); + assert.ok(str.indexOf('href="https://example.com"') > -1); + }); }); describe('sheet range limits', function() { [ ["biff2", "IV16384"], diff --git a/test.mts b/test.mts index c857291..01a7d0e 100644 --- a/test.mts +++ b/test.mts @@ -1480,6 +1480,21 @@ describe('write features', function() { var str = X.write(wb, {bookType:"html", type:"binary"}); assert.ok(str.indexOf("abc") > 0); }); + it('should remove javascript: URLs when sanitizeLinks is true', function() { + var sheet = X.utils.aoa_to_sheet([["Click me"]]); + get_cell(sheet, "A1").l = { Target: "javascript:alert('xss')" }; + + assert.ok(X.utils.sheet_to_html(sheet).indexOf("javascript:alert") > -1, "javascript: should not be stripped by default"); + + assert.ok(X.utils.sheet_to_html(sheet, { sanitizeLinks: true }).indexOf("javascript:alert") === -1, "javascript: should be stripped with sanitizeLinks"); + }); + it('should preserve non-javascript URLs with sanitizeLinks', function() { + var sheet = X.utils.aoa_to_sheet([["Link"]]); + get_cell(sheet, "A1").l = { Target: "https://example.com" }; + + var str = X.utils.sheet_to_html(sheet, { sanitizeLinks: true }); + assert.ok(str.indexOf('href="https://example.com"') > -1); + }); }); describe('sheet range limits', function() { var b = ([ ["biff2", "IV16384"], diff --git a/test.test.mjs b/test.test.mjs index 4a0cece..a7103fe 100644 --- a/test.test.mjs +++ b/test.test.mjs @@ -1533,6 +1533,21 @@ describe('write features', function() { var str = X.write(wb, {bookType:"html", type:"binary"}); assert.ok(str.indexOf("abc") > 0); }); + it('should remove javascript: URLs when sanitizeLinks is true', function() { + var sheet = X.utils.aoa_to_sheet([["Click me"]]); + get_cell(sheet, "A1").l = { Target: "javascript:alert('xss')" }; + + assert.ok(X.utils.sheet_to_html(sheet).indexOf("javascript:alert") > -1, "javascript: should not be stripped by default"); + + assert.ok(X.utils.sheet_to_html(sheet, { sanitizeLinks: true }).indexOf("javascript:alert") === -1, "javascript: should be stripped with sanitizeLinks"); + }); + it('should preserve non-javascript URLs with sanitizeLinks', function() { + var sheet = X.utils.aoa_to_sheet([["Link"]]); + get_cell(sheet, "A1").l = { Target: "https://example.com" }; + + var str = X.utils.sheet_to_html(sheet, { sanitizeLinks: true }); + assert.ok(str.indexOf('href="https://example.com"') > -1); + }); }); describe('sheet range limits', function() { [ ["biff2", "IV16384"], diff --git a/test.ts b/test.ts index de40606..b5d5a53 100644 --- a/test.ts +++ b/test.ts @@ -1480,6 +1480,21 @@ Deno.test('write features', async function(t) { var str = X.write(wb, {bookType:"html", type:"binary"}); assert.assert(str.indexOf("abc") > 0); }); + await t.step('should remove javascript: URLs when sanitizeLinks is true', async function(t) { + var sheet = X.utils.aoa_to_sheet([["Click me"]]); + get_cell(sheet, "A1").l = { Target: "javascript:alert('xss')" }; + + assert.assert(X.utils.sheet_to_html(sheet).indexOf("javascript:alert") > -1, "javascript: should not be stripped by default"); + + assert.assert(X.utils.sheet_to_html(sheet, { sanitizeLinks: true }).indexOf("javascript:alert") === -1, "javascript: should be stripped with sanitizeLinks"); + }); + await t.step('should preserve non-javascript URLs with sanitizeLinks', async function(t) { + var sheet = X.utils.aoa_to_sheet([["Link"]]); + get_cell(sheet, "A1").l = { Target: "https://example.com" }; + + var str = X.utils.sheet_to_html(sheet, { sanitizeLinks: true }); + assert.assert(str.indexOf('href="https://example.com"') > -1); + }); }); await t.step('sheet range limits', async function(t) { var b = ([ ["biff2", "IV16384"], diff --git a/testnocp.ts b/testnocp.ts index ddedbb3..1c14cfd 100644 --- a/testnocp.ts +++ b/testnocp.ts @@ -1479,6 +1479,21 @@ Deno.test('write features', async function(t) { var str = X.write(wb, {bookType:"html", type:"binary"}); assert.assert(str.indexOf("abc") > 0); }); + await t.step('should remove javascript: URLs when sanitizeLinks is true', async function(t) { + var sheet = X.utils.aoa_to_sheet([["Click me"]]); + get_cell(sheet, "A1").l = { Target: "javascript:alert('xss')" }; + + assert.assert(X.utils.sheet_to_html(sheet).indexOf("javascript:alert") > -1, "javascript: should not be stripped by default"); + + assert.assert(X.utils.sheet_to_html(sheet, { sanitizeLinks: true }).indexOf("javascript:alert") === -1, "javascript: should be stripped with sanitizeLinks"); + }); + await t.step('should preserve non-javascript URLs with sanitizeLinks', async function(t) { + var sheet = X.utils.aoa_to_sheet([["Link"]]); + get_cell(sheet, "A1").l = { Target: "https://example.com" }; + + var str = X.utils.sheet_to_html(sheet, { sanitizeLinks: true }); + assert.assert(str.indexOf('href="https://example.com"') > -1); + }); }); await t.step('sheet range limits', async function(t) { var b = ([ ["biff2", "IV16384"], diff --git a/tests/core.js b/tests/core.js index 730f009..9383400 100644 --- a/tests/core.js +++ b/tests/core.js @@ -1529,6 +1529,21 @@ describe('write features', function() { var str = X.write(wb, {bookType:"html", type:"binary"}); assert.ok(str.indexOf("abc") > 0); }); + it('should remove javascript: URLs when sanitizeLinks is true', function() { + var sheet = X.utils.aoa_to_sheet([["Click me"]]); + get_cell(sheet, "A1").l = { Target: "javascript:alert('xss')" }; + + assert.ok(X.utils.sheet_to_html(sheet).indexOf("javascript:alert") > -1, "javascript: should not be stripped by default"); + + assert.ok(X.utils.sheet_to_html(sheet, { sanitizeLinks: true }).indexOf("javascript:alert") === -1, "javascript: should be stripped with sanitizeLinks"); + }); + it('should preserve non-javascript URLs with sanitizeLinks', function() { + var sheet = X.utils.aoa_to_sheet([["Link"]]); + get_cell(sheet, "A1").l = { Target: "https://example.com" }; + + var str = X.utils.sheet_to_html(sheet, { sanitizeLinks: true }); + assert.ok(str.indexOf('href="https://example.com"') > -1); + }); }); describe('sheet range limits', function() { [ ["biff2", "IV16384"], diff --git a/tests/fs_.js b/tests/fs_.js index b059219..f930396 100644 --- a/tests/fs_.js +++ b/tests/fs_.js @@ -1,5 +1,5 @@ -var assert = function(bool) { if(!bool) { throw new Error("failed assert"); } }; -assert.ok = function(bool) { if(!bool) { throw new Error("failed assert"); } }; +var assert = function(bool, msg) { if(!bool) { throw new Error(msg || "failed assert"); } }; +assert.ok = function(bool, msg) { if(!bool) { throw new Error(msg || "failed assert"); } }; assert.deepEqualArray = function(x,y) { if(x.length != y.length) throw new Error("Length mismatch: " + x.length + " != " + y.length); for(var i = 0; i < x.length; ++i) assert.deepEqual(x[i], y[i]); @@ -13,8 +13,8 @@ assert.deepEqual = function(x,y) { }; assert.notEqual = function(x,y) { if(x == y) throw new Error(x + " == " + y); }; assert.equal = function(x,y) { if(x != y) throw new Error(x + " !== " + y); }; -assert.throws = function(cb) { var pass = true; try { cb(); pass = false; } catch(e) { } if(!pass) throw new Error("Function did not throw"); }; -assert.doesNotThrow = function(cb) { var pass = true; try { cb(); } catch(e) { pass = false; } if(!pass) throw new Error("Function did throw"); }; +assert.throws = function(cb, msg) { var pass = true; try { cb(); pass = false; } catch(e) { } if(!pass) throw new Error(msg || "Function did not throw"); }; +assert.doesNotThrow = function(cb, msg) { var pass = true; try { cb(); } catch(e) { pass = false; } if(!pass) throw new Error(msg || "Function did throw"); }; function require(s) { switch(s) { diff --git a/types/index.d.ts b/types/index.d.ts index c75f4ae..e00246f 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -800,6 +800,9 @@ export interface Sheet2HTMLOpts { /** Footer HTML */ footer?: string; + + /** If true, remove javascript: URLs from hyperlinks */ + sanitizeLinks?: boolean; } export interface Sheet2JSONOpts extends DateNFOption {