159 lines
5.0 KiB
JavaScript
159 lines
5.0 KiB
JavaScript
import { DANGEROUS_PROPERTY_NAMES, criticalProperties } from "../util.js";
|
|
|
|
const defaultOnDangerousProperty = (name) => {
|
|
if (DANGEROUS_PROPERTY_NAMES.includes(name)) {
|
|
return "__" + name;
|
|
}
|
|
return name;
|
|
};
|
|
|
|
|
|
export const defaultOptions = {
|
|
preserveOrder: false,
|
|
attributeNamePrefix: '@_',
|
|
attributesGroupName: false,
|
|
textNodeName: '#text',
|
|
ignoreAttributes: true,
|
|
removeNSPrefix: false, // remove NS from tag name or attribute name if true
|
|
allowBooleanAttributes: false, //a tag can have attributes without any value
|
|
//ignoreRootElement : false,
|
|
parseTagValue: true,
|
|
parseAttributeValue: false,
|
|
trimValues: true, //Trim string values of tag and attributes
|
|
cdataPropName: false,
|
|
numberParseOptions: {
|
|
hex: true,
|
|
leadingZeros: true,
|
|
eNotation: true
|
|
},
|
|
tagValueProcessor: function (tagName, val) {
|
|
return val;
|
|
},
|
|
attributeValueProcessor: function (attrName, val) {
|
|
return val;
|
|
},
|
|
stopNodes: [], //nested tags will not be parsed even for errors
|
|
alwaysCreateTextNode: false,
|
|
isArray: () => false,
|
|
commentPropName: false,
|
|
unpairedTags: [],
|
|
processEntities: true,
|
|
htmlEntities: false,
|
|
ignoreDeclaration: false,
|
|
ignorePiTags: false,
|
|
transformTagName: false,
|
|
transformAttributeName: false,
|
|
updateTag: function (tagName, jPath, attrs) {
|
|
return tagName
|
|
},
|
|
// skipEmptyListItem: false
|
|
captureMetaData: false,
|
|
maxNestedTags: 100,
|
|
strictReservedNames: true,
|
|
jPath: true, // if true, pass jPath string to callbacks; if false, pass matcher instance
|
|
onDangerousProperty: defaultOnDangerousProperty
|
|
};
|
|
|
|
|
|
/**
|
|
* Validates that a property name is safe to use
|
|
* @param {string} propertyName - The property name to validate
|
|
* @param {string} optionName - The option field name (for error message)
|
|
* @throws {Error} If property name is dangerous
|
|
*/
|
|
function validatePropertyName(propertyName, optionName) {
|
|
if (typeof propertyName !== 'string') {
|
|
return; // Only validate string property names
|
|
}
|
|
|
|
const normalized = propertyName.toLowerCase();
|
|
if (DANGEROUS_PROPERTY_NAMES.some(dangerous => normalized === dangerous.toLowerCase())) {
|
|
throw new Error(
|
|
`[SECURITY] Invalid ${optionName}: "${propertyName}" is a reserved JavaScript keyword that could cause prototype pollution`
|
|
);
|
|
}
|
|
|
|
if (criticalProperties.some(dangerous => normalized === dangerous.toLowerCase())) {
|
|
throw new Error(
|
|
`[SECURITY] Invalid ${optionName}: "${propertyName}" is a reserved JavaScript keyword that could cause prototype pollution`
|
|
);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Normalizes processEntities option for backward compatibility
|
|
* @param {boolean|object} value
|
|
* @returns {object} Always returns normalized object
|
|
*/
|
|
function normalizeProcessEntities(value) {
|
|
// Boolean backward compatibility
|
|
if (typeof value === 'boolean') {
|
|
return {
|
|
enabled: value, // true or false
|
|
maxEntitySize: 10000,
|
|
maxExpansionDepth: 10,
|
|
maxTotalExpansions: 1000,
|
|
maxExpandedLength: 100000,
|
|
maxEntityCount: 100,
|
|
allowedTags: null,
|
|
tagFilter: null
|
|
};
|
|
}
|
|
|
|
// Object config - merge with defaults
|
|
if (typeof value === 'object' && value !== null) {
|
|
return {
|
|
enabled: value.enabled !== false,
|
|
maxEntitySize: Math.max(1, value.maxEntitySize ?? 10000),
|
|
maxExpansionDepth: Math.max(1, value.maxExpansionDepth ?? 10),
|
|
maxTotalExpansions: Math.max(1, value.maxTotalExpansions ?? 1000),
|
|
maxExpandedLength: Math.max(1, value.maxExpandedLength ?? 100000),
|
|
maxEntityCount: Math.max(1, value.maxEntityCount ?? 100),
|
|
allowedTags: value.allowedTags ?? null,
|
|
tagFilter: value.tagFilter ?? null
|
|
};
|
|
}
|
|
|
|
// Default to enabled with limits
|
|
return normalizeProcessEntities(true);
|
|
}
|
|
|
|
export const buildOptions = function (options) {
|
|
const built = Object.assign({}, defaultOptions, options);
|
|
|
|
// Validate property names to prevent prototype pollution
|
|
const propertyNameOptions = [
|
|
{ value: built.attributeNamePrefix, name: 'attributeNamePrefix' },
|
|
{ value: built.attributesGroupName, name: 'attributesGroupName' },
|
|
{ value: built.textNodeName, name: 'textNodeName' },
|
|
{ value: built.cdataPropName, name: 'cdataPropName' },
|
|
{ value: built.commentPropName, name: 'commentPropName' }
|
|
];
|
|
|
|
for (const { value, name } of propertyNameOptions) {
|
|
if (value) {
|
|
validatePropertyName(value, name);
|
|
}
|
|
}
|
|
|
|
if (built.onDangerousProperty === null) {
|
|
built.onDangerousProperty = defaultOnDangerousProperty;
|
|
}
|
|
|
|
// Always normalize processEntities for backward compatibility and validation
|
|
built.processEntities = normalizeProcessEntities(built.processEntities);
|
|
|
|
// Convert old-style stopNodes for backward compatibility
|
|
if (built.stopNodes && Array.isArray(built.stopNodes)) {
|
|
built.stopNodes = built.stopNodes.map(node => {
|
|
if (typeof node === 'string' && node.startsWith('*.')) {
|
|
// Old syntax: *.tagname meant "tagname anywhere"
|
|
// Convert to new syntax: ..tagname
|
|
return '..' + node.substring(2);
|
|
}
|
|
return node;
|
|
});
|
|
}
|
|
//console.debug(built.processEntities)
|
|
return built;
|
|
}; |