initial commit
This commit is contained in:
+159
@@ -0,0 +1,159 @@
|
||||
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;
|
||||
};
|
||||
Reference in New Issue
Block a user