diff --git a/src/lg-utils.ts b/src/lg-utils.ts
index ccedbcf71..dad6d9c88 100644
--- a/src/lg-utils.ts
+++ b/src/lg-utils.ts
@@ -388,6 +388,20 @@ const utils = {
return transform;
},
+ /**
+ * Escapes HTML attribute values to prevent XSS attacks
+ * @param {string} value - The attribute value to escape
+ * @returns {string} The escaped attribute value
+ */
+ escapeHtml(value: string): string {
+ return value
+ .replace(/&/g, '&')
+ .replace(//g, '>')
+ .replace(/"/g, '"')
+ .replace(/'/g, ''');
+ },
+
getIframeMarkup(
iframeWidth: string,
iframeHeight: string,
@@ -396,7 +410,9 @@ const utils = {
src?: string,
iframeTitle?: string,
): string {
- const title = iframeTitle ? 'title="' + iframeTitle + '"' : '';
+ const title = iframeTitle
+ ? 'title="' + this.escapeHtml(iframeTitle) + '"'
+ : '';
return `
`;
@@ -410,8 +426,8 @@ const utils = {
sizes?: string,
sources?: ImageSources[],
): string {
- const srcsetAttr = srcset ? `srcset="${srcset}"` : '';
- const sizesAttr = sizes ? `sizes="${sizes}"` : '';
+ const srcsetAttr = srcset ? `srcset="${this.escapeHtml(srcset)}"` : '';
+ const sizesAttr = sizes ? `sizes="${this.escapeHtml(sizes)}"` : '';
const imgMarkup = `
`;
let sourceTag = '';
if (sources) {
@@ -574,7 +590,11 @@ const utils = {
dynamicEl.thumb = thumb;
if (getCaptionFromTitleOrAlt && !dynamicEl.subHtml) {
- dynamicEl.subHtml = title || alt || '';
+ // Escape HTML when using title/alt as caption to prevent XSS
+ const captionText = title || alt || '';
+ dynamicEl.subHtml = captionText
+ ? this.escapeHtml(captionText)
+ : '';
}
dynamicEl.alt = alt || title || '';
dynamicElements.push(dynamicEl);
diff --git a/src/lightgallery.ts b/src/lightgallery.ts
index c36fd5fcb..02973e004 100644
--- a/src/lightgallery.ts
+++ b/src/lightgallery.ts
@@ -1004,13 +1004,13 @@ export class LightGallery {
// Use the thumbnail as dummy image which will be resized to actual image size and
// displayed on top of actual image
let imgContent: string | HTMLImageElement = '';
- const altAttr = alt ? 'alt="' + alt + '"' : '';
+ const altAttr = alt ? 'alt="' + utils.escapeHtml(alt) + '"' : '';
if (this.isFirstSlideWithZoomAnimation()) {
imgContent = this.getDummyImageContent(
$currentSlide,
index,
- altAttr,
+ alt || '',
);
} else {
imgContent = utils.getImgMarkup(
diff --git a/test/lightgallery.test.ts b/test/lightgallery.test.ts
index f40186826..84f8a0643 100644
--- a/test/lightgallery.test.ts
+++ b/test/lightgallery.test.ts
@@ -302,3 +302,222 @@ describe('Plugins', () => {
expect(LG.galleryItems[0].poster).toBeUndefined();
});
});
+
+describe('Security - XSS Prevention', () => {
+ it('Should escape double quotes in alt attribute to prevent attribute breakout', async () => {
+ // Test: Double quote to break out of attribute and inject malicious HTML
+ document.body.innerHTML = ``;
+
+ const LG = lightGallery(
+ document.getElementById('lightGallery') as HTMLElement,
+ );
+ LG.openGallery(0);
+
+ await waitFor(() => {
+ const lgObject = document.querySelector('.lg-object');
+ expect(lgObject).toBeInTheDocument();
+
+ if (lgObject) {
+ const altValue = lgObject.getAttribute('alt');
+ expect(altValue).toBe('">
');
+ }
+
+ // Verify no malicious img was injected
+ const xssImg = document.querySelector('img[src="x"]');
+ expect(xssImg).toBeNull();
+ });
+
+ LG.destroy();
+ });
+
+ it('Should escape double quotes in alt to prevent event handler injection', async () => {
+ // Test: Inject onload event handler
+ document.body.innerHTML = ``;
+
+ const LG = lightGallery(
+ document.getElementById('lightGallery') as HTMLElement,
+ );
+ LG.openGallery(0);
+
+ await waitFor(() => {
+ const lgObject = document.querySelector('.lg-object');
+ expect(lgObject).toBeInTheDocument();
+
+ if (lgObject) {
+ const altValue = lgObject.getAttribute('alt');
+ expect(altValue).toBe('" onload="alert(1)" x="');
+
+ // The onload should not be an actual attribute
+ expect(lgObject.hasAttribute('onload')).toBe(false);
+ }
+ });
+
+ LG.destroy();
+ });
+
+ it('Should escape double quotes in alt to prevent script injection', async () => {
+ // Test: Script tag injection
+ document.body.innerHTML = ``;
+
+ const LG = lightGallery(
+ document.getElementById('lightGallery') as HTMLElement,
+ );
+ LG.openGallery(0);
+
+ await waitFor(() => {
+ const lgObject = document.querySelector('.lg-object');
+ expect(lgObject).toBeInTheDocument();
+
+ // No script tags should be injected
+ const scripts = document.querySelectorAll('.lg-item script');
+ expect(scripts.length).toBe(0);
+ });
+
+ LG.destroy();
+ });
+
+ it('Should handle ampersands in alt without double-encoding', async () => {
+ // Test: Special HTML characters
+ document.body.innerHTML = ``;
+
+ const LG = lightGallery(
+ document.getElementById('lightGallery') as HTMLElement,
+ );
+ LG.openGallery(0);
+
+ await waitFor(() => {
+ const lgObject = document.querySelector('.lg-object');
+ expect(lgObject).toBeInTheDocument();
+
+ if (lgObject) {
+ const altValue = lgObject.getAttribute('alt');
+ // Browser automatically decodes HTML entities in attributes
+ expect(altValue).toBe('Testing & signs < and > symbols');
+ }
+ });
+
+ LG.destroy();
+ });
+
+ it('Should handle single quotes in alt attribute', async () => {
+ // Test: Single quotes (less dangerous but should still work)
+ document.body.innerHTML = ``;
+
+ const LG = lightGallery(
+ document.getElementById('lightGallery') as HTMLElement,
+ );
+ LG.openGallery(0);
+
+ await waitFor(() => {
+ const lgObject = document.querySelector('.lg-object');
+ expect(lgObject).toBeInTheDocument();
+
+ if (lgObject) {
+ const altValue = lgObject.getAttribute('alt');
+ expect(altValue).toBe("It's a nice image with 'quotes'");
+ }
+ });
+
+ LG.destroy();
+ });
+
+ it('Should escape double quotes in srcset attribute', async () => {
+ // Test: Srcset attribute vulnerability
+ document.body.innerHTML = ``;
+
+ const LG = lightGallery(
+ document.getElementById('lightGallery') as HTMLElement,
+ );
+ LG.openGallery(0);
+
+ await waitFor(() => {
+ const lgObject = document.querySelector('.lg-object');
+ expect(lgObject).toBeInTheDocument();
+
+ if (lgObject) {
+ // Should not have onload as an actual attribute
+ expect(lgObject.hasAttribute('onload')).toBe(false);
+ }
+ });
+
+ LG.destroy();
+ });
+
+ it('Should escape double quotes in sizes attribute', async () => {
+ // Test: Sizes attribute vulnerability
+ document.body.innerHTML = ``;
+
+ const LG = lightGallery(
+ document.getElementById('lightGallery') as HTMLElement,
+ );
+ LG.openGallery(0);
+
+ await waitFor(() => {
+ const lgObject = document.querySelector('.lg-object');
+ expect(lgObject).toBeInTheDocument();
+
+ if (lgObject) {
+ // Should not have onload as an actual attribute
+ expect(lgObject.hasAttribute('onload')).toBe(false);
+ }
+ });
+
+ LG.destroy();
+ });
+
+ it('Should handle backslashes and special characters in alt', async () => {
+ // Test: Various escape characters
+ document.body.innerHTML = ``;
+
+ const LG = lightGallery(
+ document.getElementById('lightGallery') as HTMLElement,
+ );
+ LG.openGallery(0);
+
+ await waitFor(() => {
+ const lgObject = document.querySelector('.lg-object');
+ expect(lgObject).toBeInTheDocument();
+
+ if (lgObject) {
+ const altValue = lgObject.getAttribute('alt');
+ // Should preserve the content as-is
+ expect(altValue).toContain('backslash');
+ expect(altValue).toContain('quotes');
+ }
+ });
+
+ LG.destroy();
+ });
+});