Skip to content

Commit

Permalink
fix: [1706] Disallow invalid attribute names
Browse files Browse the repository at this point in the history
  • Loading branch information
OlaviSau committed Jan 31, 2025
1 parent a72b016 commit 55ca741
Show file tree
Hide file tree
Showing 3 changed files with 207 additions and 0 deletions.
7 changes: 7 additions & 0 deletions packages/happy-dom/src/nodes/element/Element.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ import NodeFactory from '../NodeFactory.js';
import HTMLSerializer from '../../html-serializer/HTMLSerializer.js';
import HTMLParser from '../../html-parser/HTMLParser.js';
import IScrollToOptions from '../../window/IScrollToOptions.js';
import { AttributeUtility } from '../../utilities/AttributeUtility.js';

type InsertAdjacentPosition = 'beforebegin' | 'afterbegin' | 'beforeend' | 'afterend';

Expand Down Expand Up @@ -669,6 +670,12 @@ export default class Element
* @param value Value.
*/
public setAttribute(name: string, value: string): void {
AttributeUtility.validateAttributeName(
name,
this[PropertySymbol.ownerDocument][PropertySymbol.contentType],
{ method: 'setAttribute', instance: 'Element' }
);
name = String(name);
const namespaceURI = this[PropertySymbol.namespaceURI];
// TODO: Is it correct to check for namespaceURI === NamespaceURI.svg?
const attribute =
Expand Down
43 changes: 43 additions & 0 deletions packages/happy-dom/src/utilities/AttributeUtility.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import DOMException from '../exception/DOMException.js';
import DOMExceptionNameEnum from '../exception/DOMExceptionNameEnum.js';

const HTML_INVALID_ATTRIBUTE_NAME_CHARACTER_REGEX =
/[\x00-\x1F\x7F\x80-\x9F "\'><=\/\uFDD0-\uFDEF\uFFFE\uFFFF\u1FFFE\u1FFFF\u2FFFE\u2FFFF\u3FFFE\u3FFFF\u4FFFE\u4FFFF\u5FFFE\u5FFFF\u6FFFE\u6FFFF\u7FFFE\u7FFFF\u8FFFE\u8FFFF\u9FFFE\u9FFFF\uAFFFE\uAFFFF\uBFFFE\uBFFFF\uCFFFE\uCFFFF\uDFFFE\uDFFFF\uEFFFE\uEFFFF\uFFFFE\uFFFFF\u10FFFE\u10FFFF]/;

/**
* Attribute utility
*/
export class AttributeUtility {
/**
*
* @param name the attribute name
* @param contentType the attribute has to be valid in
* @param context the context in which the error occurred in
* @param context.method
* @param context.instance
*/
public static validateAttributeName(
name: unknown,
contentType: string,
context: {
method: string;
instance: string;
}
): void {
const { method, instance } = context;
if (contentType === 'text/html') {
const normalizedName = String(name).toLowerCase();
if (
HTML_INVALID_ATTRIBUTE_NAME_CHARACTER_REGEX.test(normalizedName) ||
normalizedName.length === 0 ||
normalizedName[0] === '-'
) {
throw new DOMException(
`Uncaught InvalidCharacterError: Failed to execute '${method}' on '${instance}': '${name}' is not a valid attribute name.`,
DOMExceptionNameEnum.invalidCharacterError
);
}
}
// TODO: implement XML and other content types
}
}
157 changes: 157 additions & 0 deletions packages/happy-dom/test/nodes/element/Element.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import NodeList from '../../../src/nodes/node/NodeList.js';
import Event from '../../../src/event/Event.js';
import { beforeEach, afterEach, describe, it, expect, vi } from 'vitest';
import * as PropertySymbol from '../../../src/PropertySymbol.js';
import DOMExceptionNameEnum from '../../../lib/exception/DOMExceptionNameEnum';

const NAMESPACE_URI = 'https://test.test';

Expand Down Expand Up @@ -1566,6 +1567,162 @@ describe('Element', () => {
expect(element.attributes['key2'].ownerElement === element).toBe(true);
expect(element.attributes['key2'].ownerDocument === document).toBe(true);
});

it('Sets valid attribute names', () => {
// ✅ Basic letters (lowercase & uppercase)
element.setAttribute(`abc`, '1');
expect(element.getAttribute('abc')).toBe('1'); // lowercase letters

element.setAttribute(`ABC`, '1');
expect(element.getAttribute('ABC')).toBe('1'); // uppercase letters

element.setAttribute(`AbC`, '1');
expect(element.getAttribute('AbC')).toBe('1'); // mixed case

// ✅ Length variations
element.setAttribute(`a`, '1');
expect(element.getAttribute('a')).toBe('1'); // single character

element.setAttribute(`ab`, '1');
expect(element.getAttribute('ab')).toBe('1'); // two characters

element.setAttribute(`attribute`, '1');
expect(element.getAttribute('attribute')).toBe('1'); // common length

element.setAttribute(`averyverylongattributenamethatisvalid`, '1');
expect(element.getAttribute('averyverylongattributenamethatisvalid')).toBe('1'); // long attribute name

// ✅ Attribute names with digits
element.setAttribute(`attr1`, '1');
expect(element.getAttribute('attr1')).toBe('1'); // digit at the end

element.setAttribute(`a123`, '1');
expect(element.getAttribute('a123')).toBe('1'); // multiple digits at the end

element.setAttribute(`x9y`, '1');
expect(element.getAttribute('x9y')).toBe('1'); // digit in the middle

// ✅ Attribute names with allowed special characters
element.setAttribute(`_underscore`, '1');
expect(element.getAttribute('_underscore')).toBe('1'); // starts with underscore

element.setAttribute(`under_score`, '1');
expect(element.getAttribute('under_score')).toBe('1'); // contains underscore

element.setAttribute(`hyphen-ated`, '1');
expect(element.getAttribute('hyphen-ated')).toBe('1'); // contains hyphen

element.setAttribute(`ns:attribute`, '1');
expect(element.getAttribute('ns:attribute')).toBe('1'); // namespace-style (colon allowed)

// ✅ Unicode-based attribute names
element.setAttribute(`ö`, '1');
expect(element.getAttribute('ö')).toBe('1'); // Latin extended

element.setAttribute(`ñ`, '1');
expect(element.getAttribute('ñ')).toBe('1'); // Spanish tilde-n

element.setAttribute(`名`, '1');
expect(element.getAttribute('名')).toBe('1'); // Chinese character

element.setAttribute(`имя`, '1');
expect(element.getAttribute('имя')).toBe('1'); // Cyrillic (Russian)

element.setAttribute(`أسم`, '1');
expect(element.getAttribute('أسم')).toBe('1'); // Arabic script

element.setAttribute(`𝒜𝒷𝒸`, '1');
expect(element.getAttribute('𝒜𝒷𝒸')).toBe('1'); // Unicode math letters

element.setAttribute(`ⓐⓑⓒ`, '1');
expect(element.getAttribute('ⓐⓑⓒ')).toBe('1'); // Enclosed alphanumerics

element.setAttribute(`Ωμέγα`, '1');
expect(element.getAttribute('Ωμέγα')).toBe('1'); // Greek letters

// ✅ Edge cases
element.setAttribute(`a`, '1');
expect(element.getAttribute('a')).toBe('1'); // single lowercase letter

element.setAttribute(`Z`, '1');
expect(element.getAttribute('Z')).toBe('1'); // single uppercase letter

element.setAttribute(`_`, '1');
expect(element.getAttribute('_')).toBe('1'); // single underscore (valid but unusual)

// TODO: retest in XML content type
// element.setAttribute(`:`, '1');
// expect(element.getAttribute(':')).toBe('1'); // single colon (valid in XML namespaces)
// element.setAttribute(`-`, '1');
// expect(element.getAttribute('-')).toBe('1'); // single hyphen (valid in XML but discouraged in HTML)
// element.setAttribute(`-hyphen`, '1');
// expect(element.getAttribute('-hyphen')).toBe('1'); // starts with hyphen (allowed in XML)

element.setAttribute(`valid-attribute-name-123`, '1');
expect(element.getAttribute('valid-attribute-name-123')).toBe('1'); // mixed with hyphens and digits

element.setAttribute(`data-custom`, '1');
expect(element.getAttribute('data-custom')).toBe('1'); // common custom attribute pattern
});

it('Throws an error when given an invalid character in the attribute name', () => {
try {
element.setAttribute('☺', '1');
} catch (error) {
expect(error.name).toBe(DOMExceptionNameEnum.invalidCharacterError);
}
try {
// eslint-disable-next-line
element.setAttribute({} as string, '1');
} catch (error) {
expect(error.name).toBe(DOMExceptionNameEnum.invalidCharacterError);
}
try {
element.setAttribute('', '1');
} catch (error) {
expect(error.name).toBe(DOMExceptionNameEnum.invalidCharacterError);
}
try {
element.setAttribute('=', '1');
} catch (error) {
expect(error.name).toBe(DOMExceptionNameEnum.invalidCharacterError);
}
try {
element.setAttribute(' ', '1');
} catch (error) {
expect(error.name).toBe(DOMExceptionNameEnum.invalidCharacterError);
}
try {
element.setAttribute('"', '1');
} catch (error) {
expect(error.name).toBe(DOMExceptionNameEnum.invalidCharacterError);
}
try {
element.setAttribute(`'`, '1');
} catch (error) {
expect(error.name).toBe(DOMExceptionNameEnum.invalidCharacterError);
}
try {
element.setAttribute(`>`, '1');
} catch (error) {
expect(error.name).toBe(DOMExceptionNameEnum.invalidCharacterError);
}
try {
element.setAttribute(`\/`, '1');
} catch (error) {
expect(error.name).toBe(DOMExceptionNameEnum.invalidCharacterError);
}
try {
element.setAttribute(`\u007F`, '1'); // control character delete
} catch (error) {
expect(error.name).toBe(DOMExceptionNameEnum.invalidCharacterError);
}
try {
element.setAttribute(`\u9FFFE`, '1'); // non character
} catch (error) {
expect(error.name).toBe(DOMExceptionNameEnum.invalidCharacterError);
}
});
});

describe('setAttributeNS()', () => {
Expand Down

0 comments on commit 55ca741

Please sign in to comment.