sheet_to_html sanitize links option (fixes #3345)

This commit is contained in:
SheetJS 2026-02-09 03:38:30 -05:00
parent 789e2d6870
commit d2f2e17963
10 changed files with 113 additions and 5 deletions

@ -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 = '<a href="' + escapehtml(cell.l.Target) +'">' + w + '</a>';
if(cell.l && (cell.l.Target || "#").charAt(0) != "#" && (!o.sanitizeLinks || (cell.l.Target || "").slice(0, 11).toLowerCase() != 'javascript:')) w = '<a href="' + escapehtml(cell.l.Target) +'">' + w + '</a>';
}
sp.id = (o.id || "sjs") + "-" + coord;
oo.push(writextag('td', w, sp));

15
test.js

@ -1529,6 +1529,21 @@ describe('write features', function() {
var str = X.write(wb, {bookType:"html", type:"binary"});
assert.ok(str.indexOf("<b>abc</b>") > 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"],

15
test.mjs generated

@ -1533,6 +1533,21 @@ describe('write features', function() {
var str = X.write(wb, {bookType:"html", type:"binary"});
assert.ok(str.indexOf("<b>abc</b>") > 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"],

@ -1480,6 +1480,21 @@ describe('write features', function() {
var str = X.write(wb, {bookType:"html", type:"binary"});
assert.ok(str.indexOf("<b>abc</b>") > 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"],

15
test.test.mjs generated

@ -1533,6 +1533,21 @@ describe('write features', function() {
var str = X.write(wb, {bookType:"html", type:"binary"});
assert.ok(str.indexOf("<b>abc</b>") > 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"],

15
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("<b>abc</b>") > 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"],

@ -1479,6 +1479,21 @@ Deno.test('write features', async function(t) {
var str = X.write(wb, {bookType:"html", type:"binary"});
assert.assert(str.indexOf("<b>abc</b>") > 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"],

15
tests/core.js generated

@ -1529,6 +1529,21 @@ describe('write features', function() {
var str = X.write(wb, {bookType:"html", type:"binary"});
assert.ok(str.indexOf("<b>abc</b>") > 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"],

@ -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) {

3
types/index.d.ts vendored

@ -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 {