fast-xml-parser
Version:
Validate XML, Parse XML, Build XML without C/C++ based libraries
159 lines (143 loc) • 5.16 kB
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 ?? 10000),
maxTotalExpansions: Math.max(1, value.maxTotalExpansions ?? Infinity),
maxExpandedLength: Math.max(1, value.maxExpandedLength ?? 100000),
maxEntityCount: Math.max(1, value.maxEntityCount ?? 1000),
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;
};