diff --git a/dist/cleanup.js b/dist/cleanup.js index 0cac860..680d27c 100644 --- a/dist/cleanup.js +++ b/dist/cleanup.js @@ -29939,7 +29939,7 @@ function getState(name) { return process.env[`STATE_${name}`] || ''; } -const ACTION_VERSION = '1.5.0'; +const ACTION_VERSION = '1.5.1'; const INPUT_GITHUB_TOKEN = 'github-token'; const INPUT_CACHE = 'cache'; process.platform === 'linux'; @@ -30503,7 +30503,7 @@ function requireBraceExpansion () { var y = numeric(n[1]); var width = Math.max(n[0].length, n[1].length); var incr = n.length == 3 - ? Math.abs(numeric(n[2])) + ? Math.max(Math.abs(numeric(n[2])), 1) : 1; var test = lte; var reverse = y < x; @@ -43539,6 +43539,24 @@ function isExist(v) { return typeof v !== 'undefined'; } +/** + * Dangerous property names that could lead to prototype pollution or security issues + */ +const DANGEROUS_PROPERTY_NAMES = [ + // '__proto__', + // 'constructor', + // 'prototype', + 'hasOwnProperty', + 'toString', + 'valueOf', + '__defineGetter__', + '__defineSetter__', + '__lookupGetter__', + '__lookupSetter__' +]; + +const criticalProperties = ["__proto__", "constructor", "prototype"]; + const defaultOptions$2 = { allowBooleanAttributes: false, //A tag can have attributes without any value unpairedTags: [] @@ -43956,6 +43974,14 @@ function getPositionFromMatch(match) { return match.startIndex + match[1].length; } +const defaultOnDangerousProperty = (name) => { + if (DANGEROUS_PROPERTY_NAMES.includes(name)) { + return "__" + name; + } + return name; +}; + + const defaultOptions$1 = { preserveOrder: false, attributeNamePrefix: '@_', @@ -43999,8 +44025,35 @@ const defaultOptions$1 = { 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 @@ -44024,12 +44077,12 @@ function normalizeProcessEntities(value) { // Object config - merge with defaults if (typeof value === 'object' && value !== null) { return { - enabled: value.enabled !== false, // default true if not specified - maxEntitySize: value.maxEntitySize ?? 10000, - maxExpansionDepth: value.maxExpansionDepth ?? 10, - maxTotalExpansions: value.maxTotalExpansions ?? 1000, - maxExpandedLength: value.maxExpandedLength ?? 100000, - maxEntityCount: value.maxEntityCount ?? 100, + 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 }; @@ -44042,6 +44095,25 @@ function normalizeProcessEntities(value) { const buildOptions = function (options) { const built = Object.assign({}, defaultOptions$1, 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); @@ -44127,13 +44199,14 @@ class DocTypeReader { [entityName, val, i] = this.readEntityExp(xmlData, i + 1, this.suppressValidationErr); if (val.indexOf("&") === -1) { //Parameter entities are not supported if (this.options.enabled !== false && - this.options.maxEntityCount && + this.options.maxEntityCount != null && entityCount >= this.options.maxEntityCount) { throw new Error( `Entity count (${entityCount + 1}) exceeds maximum allowed (${this.options.maxEntityCount})` ); } - const escaped = entityName.replace(/[.\-+*:]/g, '\\.'); + //const escaped = entityName.replace(/[.\-+*:]/g, '\\.'); + const escaped = entityName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); entities[entityName] = { regx: RegExp(`&${escaped};`, "g"), val: val @@ -44198,11 +44271,12 @@ class DocTypeReader { i = skipWhitespace(xmlData, i); // Read entity name - let entityName = ""; + const startIndex = i; while (i < xmlData.length && !/\s/.test(xmlData[i]) && xmlData[i] !== '"' && xmlData[i] !== "'") { - entityName += xmlData[i]; i++; } + let entityName = xmlData.substring(startIndex, i); + validateEntityName(entityName); // Skip whitespace after entity name @@ -44223,7 +44297,7 @@ class DocTypeReader { // Validate entity size if (this.options.enabled !== false && - this.options.maxEntitySize && + this.options.maxEntitySize != null && entityValue.length > this.options.maxEntitySize) { throw new Error( `Entity "${entityName}" size (${entityValue.length}) exceeds maximum allowed size (${this.options.maxEntitySize})` @@ -44239,11 +44313,13 @@ class DocTypeReader { i = skipWhitespace(xmlData, i); // Read notation name - let notationName = ""; + + const startIndex = i; while (i < xmlData.length && !/\s/.test(xmlData[i])) { - notationName += xmlData[i]; i++; } + let notationName = xmlData.substring(startIndex, i); + !this.suppressValidationErr && validateEntityName(notationName); // Skip whitespace after notation name @@ -44293,10 +44369,11 @@ class DocTypeReader { } i++; + const startIndex = i; while (i < xmlData.length && xmlData[i] !== startChar) { - identifierVal += xmlData[i]; i++; } + identifierVal = xmlData.substring(startIndex, i); if (xmlData[i] !== startChar) { throw new Error(`Unterminated ${type} value`); @@ -44316,11 +44393,11 @@ class DocTypeReader { i = skipWhitespace(xmlData, i); // Read element name - let elementName = ""; + const startIndex = i; while (i < xmlData.length && !/\s/.test(xmlData[i])) { - elementName += xmlData[i]; i++; } + let elementName = xmlData.substring(startIndex, i); // Validate element name if (!this.suppressValidationErr && !isName(elementName)) { @@ -44337,10 +44414,12 @@ class DocTypeReader { i++; // Move past '(' // Read content model + const startIndex = i; while (i < xmlData.length && xmlData[i] !== ")") { - contentModel += xmlData[i]; i++; } + contentModel = xmlData.substring(startIndex, i); + if (xmlData[i] !== ")") { throw new Error("Unterminated content model"); } @@ -44361,11 +44440,11 @@ class DocTypeReader { i = skipWhitespace(xmlData, i); // Read element name - let elementName = ""; + let startIndex = i; while (i < xmlData.length && !/\s/.test(xmlData[i])) { - elementName += xmlData[i]; i++; } + let elementName = xmlData.substring(startIndex, i); // Validate element name validateEntityName(elementName); @@ -44374,11 +44453,11 @@ class DocTypeReader { i = skipWhitespace(xmlData, i); // Read attribute name - let attributeName = ""; + startIndex = i; while (i < xmlData.length && !/\s/.test(xmlData[i])) { - attributeName += xmlData[i]; i++; } + let attributeName = xmlData.substring(startIndex, i); // Validate attribute name if (!validateEntityName(attributeName)) { @@ -44406,11 +44485,13 @@ class DocTypeReader { // Read the list of allowed notations let allowedNotations = []; while (i < xmlData.length && xmlData[i] !== ")") { - let notation = ""; + + + const startIndex = i; while (i < xmlData.length && xmlData[i] !== "|" && xmlData[i] !== ")") { - notation += xmlData[i]; i++; } + let notation = xmlData.substring(startIndex, i); // Validate notation name notation = notation.trim(); @@ -44436,10 +44517,11 @@ class DocTypeReader { attributeType += " (" + allowedNotations.join("|") + ")"; } else { // Handle simple types (e.g., CDATA, ID, IDREF, etc.) + const startIndex = i; while (i < xmlData.length && !/\s/.test(xmlData[i])) { - attributeType += xmlData[i]; i++; } + attributeType += xmlData.substring(startIndex, i); // Validate simple attribute type const validTypes = ["CDATA", "ID", "IDREF", "IDREFS", "ENTITY", "ENTITIES", "NMTOKEN", "NMTOKENS"]; @@ -44520,8 +44602,9 @@ function toNumber(str, options = {}) { let trimmedStr = str.trim(); - if (options.skipLike !== undefined && options.skipLike.test(trimmedStr)) return str; - else if (str === "0") return 0; + if (trimmedStr.length === 0) return str; + else if (options.skipLike !== undefined && options.skipLike.test(trimmedStr)) return str; + else if (trimmedStr === "0") return 0; else if (options.hex && hexRegex.test(trimmedStr)) { return parse_int(trimmedStr, 16); // }else if (options.oct && octRegex.test(str)) { @@ -44597,11 +44680,16 @@ function resolveEnotation(str, trimmedStr, options) { else if (leadingZeros.length === 1 && (notation[3].startsWith(`.${eChar}`) || notation[3][0] === eChar)) { return Number(trimmedStr); - } else if (options.leadingZeros && !eAdjacentToLeadingZeros) { //accept with leading zeros - //remove leading 0s - trimmedStr = (notation[1] || "") + notation[3]; + } else if (leadingZeros.length > 0) { + // Has leading zeros — only accept if leadingZeros option allows it + if (options.leadingZeros && !eAdjacentToLeadingZeros) { + trimmedStr = (notation[1] || "") + notation[3]; + return Number(trimmedStr); + } else return str; + } else { + // No leading zeros — always valid e-notation, parse it return Number(trimmedStr); - } else return str; + } } else { return str; } @@ -44922,6 +45010,14 @@ class Expression { * const expr = new Expression("root.users.user"); * matcher.matches(expr); // true */ + +/** + * Names of methods that mutate Matcher state. + * Any attempt to call these on a read-only view throws a TypeError. + * @type {Set} + */ +const MUTATING_METHODS = new Set(['push', 'pop', 'reset', 'updateCurrent', 'restore']); + class Matcher { /** * Create a new Matcher @@ -45319,6 +45415,82 @@ class Matcher { this.path = snapshot.path.map(node => ({ ...node })); this.siblingStacks = snapshot.siblingStacks.map(map => new Map(map)); } + + /** + * Return a read-only view of this matcher. + * + * The returned object exposes all query/inspection methods but throws a + * TypeError if any state-mutating method is called (`push`, `pop`, `reset`, + * `updateCurrent`, `restore`). Property reads (e.g. `.path`, `.separator`) + * are allowed but the returned arrays/objects are frozen so callers cannot + * mutate internal state through them either. + * + * @returns {ReadOnlyMatcher} A proxy that forwards read operations and blocks writes. + * + * @example + * const matcher = new Matcher(); + * matcher.push("root", {}); + * + * const ro = matcher.readOnly(); + * ro.matches(expr); // ✓ works + * ro.getCurrentTag(); // ✓ works + * ro.push("child", {}); // ✗ throws TypeError + * ro.reset(); // ✗ throws TypeError + */ + readOnly() { + const self = this; + + return new Proxy(self, { + get(target, prop, receiver) { + // Block mutating methods + if (MUTATING_METHODS.has(prop)) { + return () => { + throw new TypeError( + `Cannot call '${prop}' on a read-only Matcher. ` + + `Obtain a writable instance to mutate state.` + ); + }; + } + + const value = Reflect.get(target, prop, receiver); + + // Freeze array/object properties so callers can't mutate internal + // state through direct property access (e.g. matcher.path.push(...)) + if (prop === 'path' || prop === 'siblingStacks') { + return Object.freeze( + Array.isArray(value) + ? value.map(item => + item instanceof Map + ? Object.freeze(new Map(item)) // freeze a copy of each Map + : Object.freeze({ ...item }) // freeze a copy of each node + ) + : value + ); + } + + // Bind methods so `this` inside them still refers to the real Matcher + if (typeof value === 'function') { + return value.bind(target); + } + + return value; + }, + + // Prevent any property assignment on the read-only view + set(_target, prop) { + throw new TypeError( + `Cannot set property '${String(prop)}' on a read-only Matcher.` + ); + }, + + // Prevent property deletion + deleteProperty(_target, prop) { + throw new TypeError( + `Cannot delete property '${String(prop)}' from a read-only Matcher.` + ); + } + }); + } } // const regx = @@ -45426,6 +45598,10 @@ class OrderedObjParser { // Initialize path matcher for path-expression-matcher this.matcher = new Matcher(); + // Live read-only proxy of matcher — PEM creates and caches this internally. + // All user callbacks receive this instead of the mutable matcher. + this.readonlyMatcher = this.matcher.readOnly(); + // Flag to track if current node is a stop node (optimization) this.isCurrentNodeStopNode = false; @@ -45538,7 +45714,7 @@ function buildAttributesMap(attrStr, jPath, tagName) { if (this.options.trimValues) { parsedVal = parsedVal.trim(); } - parsedVal = this.replaceEntitiesValue(parsedVal, tagName, jPath); + parsedVal = this.replaceEntitiesValue(parsedVal, tagName, this.readonlyMatcher); rawAttrsForMatcher[attrName] = parsedVal; } } @@ -45553,7 +45729,7 @@ function buildAttributesMap(attrStr, jPath, tagName) { const attrName = this.resolveNameSpace(matches[i][1]); // Convert jPath to string if needed for ignoreAttributesFn - const jPathStr = this.options.jPath ? jPath.toString() : jPath; + const jPathStr = this.options.jPath ? jPath.toString() : this.readonlyMatcher; if (this.ignoreAttributesFn(attrName, jPathStr)) { continue } @@ -45565,16 +45741,17 @@ function buildAttributesMap(attrStr, jPath, tagName) { if (this.options.transformAttributeName) { aName = this.options.transformAttributeName(aName); } - if (aName === "__proto__") aName = "#__proto__"; + //if (aName === "__proto__") aName = "#__proto__"; + aName = sanitizeName(aName, this.options); if (oldVal !== undefined) { if (this.options.trimValues) { oldVal = oldVal.trim(); } - oldVal = this.replaceEntitiesValue(oldVal, tagName, jPath); + oldVal = this.replaceEntitiesValue(oldVal, tagName, this.readonlyMatcher); - // Pass jPath string or matcher based on options.jPath setting - const jPathOrMatcher = this.options.jPath ? jPath.toString() : jPath; + // Pass jPath string or readonlyMatcher based on options.jPath setting + const jPathOrMatcher = this.options.jPath ? jPath.toString() : this.readonlyMatcher; const newVal = this.options.attributeValueProcessor(attrName, oldVal, jPathOrMatcher); if (newVal === null || newVal === undefined) { //don't parse @@ -45638,12 +45815,10 @@ const parseXml = function (xmlData) { } } - if (this.options.transformTagName) { - tagName = this.options.transformTagName(tagName); - } + tagName = transformTagName(this.options.transformTagName, tagName, "", this.options).tagName; if (currentNode) { - textData = this.saveTextToParentTag(textData, currentNode, this.matcher); + textData = this.saveTextToParentTag(textData, currentNode, this.readonlyMatcher); } //check if last tag of nested tag was unpaired tag @@ -45668,7 +45843,7 @@ const parseXml = function (xmlData) { let tagData = readTagExp(xmlData, i, false, "?>"); if (!tagData) throw new Error("Pi Tag is not closed."); - textData = this.saveTextToParentTag(textData, currentNode, this.matcher); + textData = this.saveTextToParentTag(textData, currentNode, this.readonlyMatcher); if ((this.options.ignoreDeclaration && tagData.tagName === "?xml") || this.options.ignorePiTags) ; else { const childNode = new XmlNode(tagData.tagName); @@ -45677,7 +45852,7 @@ const parseXml = function (xmlData) { if (tagData.tagName !== tagData.tagExp && tagData.attrExpPresent) { childNode[":@"] = this.buildAttributesMap(tagData.tagExp, this.matcher, tagData.tagName); } - this.addChild(currentNode, childNode, this.matcher, i); + this.addChild(currentNode, childNode, this.readonlyMatcher, i); } @@ -45687,7 +45862,7 @@ const parseXml = function (xmlData) { if (this.options.commentPropName) { const comment = xmlData.substring(i + 4, endIndex - 2); - textData = this.saveTextToParentTag(textData, currentNode, this.matcher); + textData = this.saveTextToParentTag(textData, currentNode, this.readonlyMatcher); currentNode.add(this.options.commentPropName, [{ [this.options.textNodeName]: comment }]); } @@ -45700,9 +45875,9 @@ const parseXml = function (xmlData) { const closeIndex = findClosingIndex(xmlData, "]]>", i, "CDATA is not closed.") - 2; const tagExp = xmlData.substring(i + 9, closeIndex); - textData = this.saveTextToParentTag(textData, currentNode, this.matcher); + textData = this.saveTextToParentTag(textData, currentNode, this.readonlyMatcher); - let val = this.parseTextData(tagExp, currentNode.tagname, this.matcher, true, false, true, true); + let val = this.parseTextData(tagExp, currentNode.tagname, this.readonlyMatcher, true, false, true, true); if (val == undefined) val = ""; //cdata should be set even if it is 0 length string @@ -45729,18 +45904,13 @@ const parseXml = function (xmlData) { let attrExpPresent = result.attrExpPresent; let closeIndex = result.closeIndex; - if (this.options.transformTagName) { - //console.log(tagExp, tagName) - const newTagName = this.options.transformTagName(tagName); - if (tagExp === tagName) { - tagExp = newTagName; - } - tagName = newTagName; - } + ({ tagName, tagExp } = transformTagName(this.options.transformTagName, tagName, tagExp, this.options)); if (this.options.strictReservedNames && (tagName === this.options.commentPropName || tagName === this.options.cdataPropName + || tagName === this.options.textNodeName + || tagName === this.options.attributesGroupName )) { throw new Error(`Invalid tag name: ${tagName}`); } @@ -45749,7 +45919,7 @@ const parseXml = function (xmlData) { if (currentNode && textData) { if (currentNode.tagname !== '!xml') { //when nested tag is found - textData = this.saveTextToParentTag(textData, currentNode, this.matcher, false); + textData = this.saveTextToParentTag(textData, currentNode, this.readonlyMatcher, false); } } @@ -45838,23 +46008,17 @@ const parseXml = function (xmlData) { this.matcher.pop(); // Pop the stop node tag this.isCurrentNodeStopNode = false; // Reset flag - this.addChild(currentNode, childNode, this.matcher, startIndex); + this.addChild(currentNode, childNode, this.readonlyMatcher, startIndex); } else { //selfClosing tag if (isSelfClosing) { - if (this.options.transformTagName) { - const newTagName = this.options.transformTagName(tagName); - if (tagExp === tagName) { - tagExp = newTagName; - } - tagName = newTagName; - } + ({ tagName, tagExp } = transformTagName(this.options.transformTagName, tagName, tagExp, this.options)); const childNode = new XmlNode(tagName); if (prefixedAttrs) { childNode[":@"] = prefixedAttrs; } - this.addChild(currentNode, childNode, this.matcher, startIndex); + this.addChild(currentNode, childNode, this.readonlyMatcher, startIndex); this.matcher.pop(); // Pop self-closing tag this.isCurrentNodeStopNode = false; // Reset flag } @@ -45863,7 +46027,7 @@ const parseXml = function (xmlData) { if (prefixedAttrs) { childNode[":@"] = prefixedAttrs; } - this.addChild(currentNode, childNode, this.matcher, startIndex); + this.addChild(currentNode, childNode, this.readonlyMatcher, startIndex); this.matcher.pop(); // Pop unpaired tag this.isCurrentNodeStopNode = false; // Reset flag i = result.closeIndex; @@ -45881,7 +46045,7 @@ const parseXml = function (xmlData) { if (prefixedAttrs) { childNode[":@"] = prefixedAttrs; } - this.addChild(currentNode, childNode, this.matcher, startIndex); + this.addChild(currentNode, childNode, this.readonlyMatcher, startIndex); currentNode = childNode; } textData = ""; @@ -45943,7 +46107,7 @@ function replaceEntitiesValue$1(val, tagName, jPath) { } // Replace DOCTYPE entities - for (let entityName in this.docTypeEntities) { + for (const entityName of Object.keys(this.docTypeEntities)) { const entity = this.docTypeEntities[entityName]; const matches = val.match(entity.regx); @@ -45975,19 +46139,38 @@ function replaceEntitiesValue$1(val, tagName, jPath) { } } } - if (val.indexOf('&') === -1) return val; // Early exit - // Replace standard entities - for (let entityName in this.lastEntities) { + for (const entityName of Object.keys(this.lastEntities)) { const entity = this.lastEntities[entityName]; + const matches = val.match(entity.regex); + if (matches) { + this.entityExpansionCount += matches.length; + if (entityConfig.maxTotalExpansions && + this.entityExpansionCount > entityConfig.maxTotalExpansions) { + throw new Error( + `Entity expansion limit exceeded: ${this.entityExpansionCount} > ${entityConfig.maxTotalExpansions}` + ); + } + } val = val.replace(entity.regex, entity.val); } - if (val.indexOf('&') === -1) return val; // Early exit + if (val.indexOf('&') === -1) return val; // Replace HTML entities if enabled if (this.options.htmlEntities) { - for (let entityName in this.htmlEntities) { + for (const entityName of Object.keys(this.htmlEntities)) { const entity = this.htmlEntities[entityName]; + const matches = val.match(entity.regex); + if (matches) { + //console.log(matches); + this.entityExpansionCount += matches.length; + if (entityConfig.maxTotalExpansions && + this.entityExpansionCount > entityConfig.maxTotalExpansions) { + throw new Error( + `Entity expansion limit exceeded: ${this.entityExpansionCount} > ${entityConfig.maxTotalExpansions}` + ); + } + } val = val.replace(entity.regex, entity.val); } } @@ -46184,6 +46367,29 @@ function fromCodePoint(str, base, prefix) { } } +function transformTagName(fn, tagName, tagExp, options) { + if (fn) { + const newTagName = fn(tagName); + if (tagExp === tagName) { + tagExp = newTagName; + } + tagName = newTagName; + } + tagName = sanitizeName(tagName, options); + return { tagName, tagExp }; +} + + + +function sanitizeName(name, options) { + if (criticalProperties.includes(name)) { + throw new Error(`[SECURITY] Invalid name: "${name}" is a reserved JavaScript keyword that could cause prototype pollution`); + } else if (DANGEROUS_PROPERTY_NAMES.includes(name)) { + return options.onDangerousProperty(name); + } + return name; +} + const METADATA_SYMBOL = XmlNode.getMetaDataSymbol(); /** @@ -46216,18 +46422,17 @@ function stripAttributePrefix(attrs, prefix) { * @param {Matcher} matcher - Path matcher instance * @returns */ -function prettify(node, options, matcher) { - return compress(node, options, matcher); +function prettify(node, options, matcher, readonlyMatcher) { + return compress(node, options, matcher, readonlyMatcher); } /** - * * @param {array} arr * @param {object} options * @param {Matcher} matcher - Path matcher instance * @returns object */ -function compress(arr, options, matcher) { +function compress(arr, options, matcher, readonlyMatcher) { let text; const compressedObj = {}; //This is intended to be a plain object for (let i = 0; i < arr.length; i++) { @@ -46250,11 +46455,11 @@ function compress(arr, options, matcher) { continue; } else if (tagObj[property]) { - let val = compress(tagObj[property], options, matcher); + let val = compress(tagObj[property], options, matcher, readonlyMatcher); const isLeaf = isLeafTag(val, options); if (tagObj[":@"]) { - assignAttributes(val, tagObj[":@"], matcher, options); + assignAttributes(val, tagObj[":@"], readonlyMatcher, options); } else if (Object.keys(val).length === 1 && val[options.textNodeName] !== undefined && !options.alwaysCreateTextNode) { val = val[options.textNodeName]; } else if (Object.keys(val).length === 0) { @@ -46276,8 +46481,8 @@ function compress(arr, options, matcher) { //TODO: if a node is not an array, then check if it should be an array //also determine if it is a leaf node - // Pass jPath string or matcher based on options.jPath setting - const jPathOrMatcher = options.jPath ? matcher.toString() : matcher; + // Pass jPath string or readonlyMatcher based on options.jPath setting + const jPathOrMatcher = options.jPath ? readonlyMatcher.toString() : readonlyMatcher; if (options.isArray(property, jPathOrMatcher, isLeaf)) { compressedObj[property] = [val]; } else { @@ -46309,7 +46514,7 @@ function propName$1(obj) { } } -function assignAttributes(obj, attrMap, matcher, options) { +function assignAttributes(obj, attrMap, readonlyMatcher, options) { if (attrMap) { const keys = Object.keys(attrMap); const len = keys.length; //don't make it inline @@ -46324,8 +46529,8 @@ function assignAttributes(obj, attrMap, matcher, options) { // For attributes, we need to create a temporary path // Pass jPath string or matcher based on options.jPath setting const jPathOrMatcher = options.jPath - ? matcher.toString() + "." + rawAttrName - : matcher; + ? readonlyMatcher.toString() + "." + rawAttrName + : readonlyMatcher; if (options.isArray(atrrName, jPathOrMatcher, true, true)) { obj[atrrName] = [attrMap[atrrName]]; @@ -46385,7 +46590,7 @@ class XMLParser { orderedObjParser.addExternalEntities(this.externalEntities); const orderedResult = orderedObjParser.parseXml(xmlData); if (this.options.preserveOrder || orderedResult === undefined) return orderedResult; - else return prettify(orderedResult, this.options, orderedObjParser.matcher); + else return prettify(orderedResult, this.options, orderedObjParser.matcher, orderedObjParser.readonlyMatcher); } /** @@ -46457,6 +46662,9 @@ function arrToStr(arr, options, indentation, matcher, stopNodeExpressions) { let xmlStr = ""; let isPreviousElementTag = false; + if (options.maxNestedTags && matcher.getDepth() > options.maxNestedTags) { + throw new Error("Maximum nested tags exceeded"); + } if (!Array.isArray(arr)) { // Non-array values (e.g. string tag values) should be treated as text content @@ -46533,6 +46741,7 @@ function arrToStr(arr, options, indentation, matcher, stopNodeExpressions) { if (isStopNode) { tagValue = getRawContent(tagObj[tagName], options); } else { + tagValue = arrToStr(tagObj[tagName], options, newIdentation, matcher, stopNodeExpressions); } @@ -46758,6 +46967,7 @@ const defaultOptions = { // transformTagName: false, // transformAttributeName: false, oneListGroup: false, + maxNestedTags: 100, jPath: true // When true, callbacks receive string jPath; when false, receive Matcher instance }; @@ -46833,7 +47043,9 @@ Builder.prototype.build = function (jObj) { Builder.prototype.j2x = function (jObj, level, matcher) { let attrStr = ''; let val = ''; - + if (this.options.maxNestedTags && matcher.getDepth() >= this.options.maxNestedTags) { + throw new Error("Maximum nested tags exceeded"); + } // Get jPath based on option: string for backward compatibility, or Matcher for new features const jPath = this.options.jPath ? matcher.toString() : matcher; diff --git a/dist/main.js b/dist/main.js index d05446c..85cfe0b 100644 --- a/dist/main.js +++ b/dist/main.js @@ -51,7 +51,7 @@ import require$$1$5 from 'tty'; import { createHmac } from 'node:crypto'; import fs$1 from 'node:fs'; -const ACTION_VERSION = '1.5.0'; +const ACTION_VERSION = '1.5.1'; const INPUT_VERSION = 'version'; const INPUT_GDS_TOKEN = 'gds-token'; const INPUT_JAVA_VERSION = 'java-version'; @@ -39314,7 +39314,7 @@ function requireBraceExpansion () { var y = numeric(n[1]); var width = Math.max(n[0].length, n[1].length); var incr = n.length == 3 - ? Math.abs(numeric(n[2])) + ? Math.max(Math.abs(numeric(n[2])), 1) : 1; var test = lte; var reverse = y < x; @@ -49661,6 +49661,24 @@ function isExist(v) { return typeof v !== 'undefined'; } +/** + * Dangerous property names that could lead to prototype pollution or security issues + */ +const DANGEROUS_PROPERTY_NAMES = [ + // '__proto__', + // 'constructor', + // 'prototype', + 'hasOwnProperty', + 'toString', + 'valueOf', + '__defineGetter__', + '__defineSetter__', + '__lookupGetter__', + '__lookupSetter__' +]; + +const criticalProperties = ["__proto__", "constructor", "prototype"]; + const defaultOptions$2 = { allowBooleanAttributes: false, //A tag can have attributes without any value unpairedTags: [] @@ -50078,6 +50096,14 @@ function getPositionFromMatch(match) { return match.startIndex + match[1].length; } +const defaultOnDangerousProperty = (name) => { + if (DANGEROUS_PROPERTY_NAMES.includes(name)) { + return "__" + name; + } + return name; +}; + + const defaultOptions$1 = { preserveOrder: false, attributeNamePrefix: '@_', @@ -50121,8 +50147,35 @@ const defaultOptions$1 = { 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 @@ -50146,12 +50199,12 @@ function normalizeProcessEntities(value) { // Object config - merge with defaults if (typeof value === 'object' && value !== null) { return { - enabled: value.enabled !== false, // default true if not specified - maxEntitySize: value.maxEntitySize ?? 10000, - maxExpansionDepth: value.maxExpansionDepth ?? 10, - maxTotalExpansions: value.maxTotalExpansions ?? 1000, - maxExpandedLength: value.maxExpandedLength ?? 100000, - maxEntityCount: value.maxEntityCount ?? 100, + 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 }; @@ -50164,6 +50217,25 @@ function normalizeProcessEntities(value) { const buildOptions = function (options) { const built = Object.assign({}, defaultOptions$1, 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); @@ -50249,13 +50321,14 @@ class DocTypeReader { [entityName, val, i] = this.readEntityExp(xmlData, i + 1, this.suppressValidationErr); if (val.indexOf("&") === -1) { //Parameter entities are not supported if (this.options.enabled !== false && - this.options.maxEntityCount && + this.options.maxEntityCount != null && entityCount >= this.options.maxEntityCount) { throw new Error( `Entity count (${entityCount + 1}) exceeds maximum allowed (${this.options.maxEntityCount})` ); } - const escaped = entityName.replace(/[.\-+*:]/g, '\\.'); + //const escaped = entityName.replace(/[.\-+*:]/g, '\\.'); + const escaped = entityName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); entities[entityName] = { regx: RegExp(`&${escaped};`, "g"), val: val @@ -50320,11 +50393,12 @@ class DocTypeReader { i = skipWhitespace(xmlData, i); // Read entity name - let entityName = ""; + const startIndex = i; while (i < xmlData.length && !/\s/.test(xmlData[i]) && xmlData[i] !== '"' && xmlData[i] !== "'") { - entityName += xmlData[i]; i++; } + let entityName = xmlData.substring(startIndex, i); + validateEntityName(entityName); // Skip whitespace after entity name @@ -50345,7 +50419,7 @@ class DocTypeReader { // Validate entity size if (this.options.enabled !== false && - this.options.maxEntitySize && + this.options.maxEntitySize != null && entityValue.length > this.options.maxEntitySize) { throw new Error( `Entity "${entityName}" size (${entityValue.length}) exceeds maximum allowed size (${this.options.maxEntitySize})` @@ -50361,11 +50435,13 @@ class DocTypeReader { i = skipWhitespace(xmlData, i); // Read notation name - let notationName = ""; + + const startIndex = i; while (i < xmlData.length && !/\s/.test(xmlData[i])) { - notationName += xmlData[i]; i++; } + let notationName = xmlData.substring(startIndex, i); + !this.suppressValidationErr && validateEntityName(notationName); // Skip whitespace after notation name @@ -50415,10 +50491,11 @@ class DocTypeReader { } i++; + const startIndex = i; while (i < xmlData.length && xmlData[i] !== startChar) { - identifierVal += xmlData[i]; i++; } + identifierVal = xmlData.substring(startIndex, i); if (xmlData[i] !== startChar) { throw new Error(`Unterminated ${type} value`); @@ -50438,11 +50515,11 @@ class DocTypeReader { i = skipWhitespace(xmlData, i); // Read element name - let elementName = ""; + const startIndex = i; while (i < xmlData.length && !/\s/.test(xmlData[i])) { - elementName += xmlData[i]; i++; } + let elementName = xmlData.substring(startIndex, i); // Validate element name if (!this.suppressValidationErr && !isName(elementName)) { @@ -50459,10 +50536,12 @@ class DocTypeReader { i++; // Move past '(' // Read content model + const startIndex = i; while (i < xmlData.length && xmlData[i] !== ")") { - contentModel += xmlData[i]; i++; } + contentModel = xmlData.substring(startIndex, i); + if (xmlData[i] !== ")") { throw new Error("Unterminated content model"); } @@ -50483,11 +50562,11 @@ class DocTypeReader { i = skipWhitespace(xmlData, i); // Read element name - let elementName = ""; + let startIndex = i; while (i < xmlData.length && !/\s/.test(xmlData[i])) { - elementName += xmlData[i]; i++; } + let elementName = xmlData.substring(startIndex, i); // Validate element name validateEntityName(elementName); @@ -50496,11 +50575,11 @@ class DocTypeReader { i = skipWhitespace(xmlData, i); // Read attribute name - let attributeName = ""; + startIndex = i; while (i < xmlData.length && !/\s/.test(xmlData[i])) { - attributeName += xmlData[i]; i++; } + let attributeName = xmlData.substring(startIndex, i); // Validate attribute name if (!validateEntityName(attributeName)) { @@ -50528,11 +50607,13 @@ class DocTypeReader { // Read the list of allowed notations let allowedNotations = []; while (i < xmlData.length && xmlData[i] !== ")") { - let notation = ""; + + + const startIndex = i; while (i < xmlData.length && xmlData[i] !== "|" && xmlData[i] !== ")") { - notation += xmlData[i]; i++; } + let notation = xmlData.substring(startIndex, i); // Validate notation name notation = notation.trim(); @@ -50558,10 +50639,11 @@ class DocTypeReader { attributeType += " (" + allowedNotations.join("|") + ")"; } else { // Handle simple types (e.g., CDATA, ID, IDREF, etc.) + const startIndex = i; while (i < xmlData.length && !/\s/.test(xmlData[i])) { - attributeType += xmlData[i]; i++; } + attributeType += xmlData.substring(startIndex, i); // Validate simple attribute type const validTypes = ["CDATA", "ID", "IDREF", "IDREFS", "ENTITY", "ENTITIES", "NMTOKEN", "NMTOKENS"]; @@ -50642,8 +50724,9 @@ function toNumber(str, options = {}) { let trimmedStr = str.trim(); - if (options.skipLike !== undefined && options.skipLike.test(trimmedStr)) return str; - else if (str === "0") return 0; + if (trimmedStr.length === 0) return str; + else if (options.skipLike !== undefined && options.skipLike.test(trimmedStr)) return str; + else if (trimmedStr === "0") return 0; else if (options.hex && hexRegex.test(trimmedStr)) { return parse_int(trimmedStr, 16); // }else if (options.oct && octRegex.test(str)) { @@ -50719,11 +50802,16 @@ function resolveEnotation(str, trimmedStr, options) { else if (leadingZeros.length === 1 && (notation[3].startsWith(`.${eChar}`) || notation[3][0] === eChar)) { return Number(trimmedStr); - } else if (options.leadingZeros && !eAdjacentToLeadingZeros) { //accept with leading zeros - //remove leading 0s - trimmedStr = (notation[1] || "") + notation[3]; + } else if (leadingZeros.length > 0) { + // Has leading zeros — only accept if leadingZeros option allows it + if (options.leadingZeros && !eAdjacentToLeadingZeros) { + trimmedStr = (notation[1] || "") + notation[3]; + return Number(trimmedStr); + } else return str; + } else { + // No leading zeros — always valid e-notation, parse it return Number(trimmedStr); - } else return str; + } } else { return str; } @@ -51044,6 +51132,14 @@ class Expression { * const expr = new Expression("root.users.user"); * matcher.matches(expr); // true */ + +/** + * Names of methods that mutate Matcher state. + * Any attempt to call these on a read-only view throws a TypeError. + * @type {Set} + */ +const MUTATING_METHODS = new Set(['push', 'pop', 'reset', 'updateCurrent', 'restore']); + class Matcher { /** * Create a new Matcher @@ -51441,6 +51537,82 @@ class Matcher { this.path = snapshot.path.map(node => ({ ...node })); this.siblingStacks = snapshot.siblingStacks.map(map => new Map(map)); } + + /** + * Return a read-only view of this matcher. + * + * The returned object exposes all query/inspection methods but throws a + * TypeError if any state-mutating method is called (`push`, `pop`, `reset`, + * `updateCurrent`, `restore`). Property reads (e.g. `.path`, `.separator`) + * are allowed but the returned arrays/objects are frozen so callers cannot + * mutate internal state through them either. + * + * @returns {ReadOnlyMatcher} A proxy that forwards read operations and blocks writes. + * + * @example + * const matcher = new Matcher(); + * matcher.push("root", {}); + * + * const ro = matcher.readOnly(); + * ro.matches(expr); // ✓ works + * ro.getCurrentTag(); // ✓ works + * ro.push("child", {}); // ✗ throws TypeError + * ro.reset(); // ✗ throws TypeError + */ + readOnly() { + const self = this; + + return new Proxy(self, { + get(target, prop, receiver) { + // Block mutating methods + if (MUTATING_METHODS.has(prop)) { + return () => { + throw new TypeError( + `Cannot call '${prop}' on a read-only Matcher. ` + + `Obtain a writable instance to mutate state.` + ); + }; + } + + const value = Reflect.get(target, prop, receiver); + + // Freeze array/object properties so callers can't mutate internal + // state through direct property access (e.g. matcher.path.push(...)) + if (prop === 'path' || prop === 'siblingStacks') { + return Object.freeze( + Array.isArray(value) + ? value.map(item => + item instanceof Map + ? Object.freeze(new Map(item)) // freeze a copy of each Map + : Object.freeze({ ...item }) // freeze a copy of each node + ) + : value + ); + } + + // Bind methods so `this` inside them still refers to the real Matcher + if (typeof value === 'function') { + return value.bind(target); + } + + return value; + }, + + // Prevent any property assignment on the read-only view + set(_target, prop) { + throw new TypeError( + `Cannot set property '${String(prop)}' on a read-only Matcher.` + ); + }, + + // Prevent property deletion + deleteProperty(_target, prop) { + throw new TypeError( + `Cannot delete property '${String(prop)}' from a read-only Matcher.` + ); + } + }); + } } // const regx = @@ -51548,6 +51720,10 @@ class OrderedObjParser { // Initialize path matcher for path-expression-matcher this.matcher = new Matcher(); + // Live read-only proxy of matcher — PEM creates and caches this internally. + // All user callbacks receive this instead of the mutable matcher. + this.readonlyMatcher = this.matcher.readOnly(); + // Flag to track if current node is a stop node (optimization) this.isCurrentNodeStopNode = false; @@ -51660,7 +51836,7 @@ function buildAttributesMap(attrStr, jPath, tagName) { if (this.options.trimValues) { parsedVal = parsedVal.trim(); } - parsedVal = this.replaceEntitiesValue(parsedVal, tagName, jPath); + parsedVal = this.replaceEntitiesValue(parsedVal, tagName, this.readonlyMatcher); rawAttrsForMatcher[attrName] = parsedVal; } } @@ -51675,7 +51851,7 @@ function buildAttributesMap(attrStr, jPath, tagName) { const attrName = this.resolveNameSpace(matches[i][1]); // Convert jPath to string if needed for ignoreAttributesFn - const jPathStr = this.options.jPath ? jPath.toString() : jPath; + const jPathStr = this.options.jPath ? jPath.toString() : this.readonlyMatcher; if (this.ignoreAttributesFn(attrName, jPathStr)) { continue } @@ -51687,16 +51863,17 @@ function buildAttributesMap(attrStr, jPath, tagName) { if (this.options.transformAttributeName) { aName = this.options.transformAttributeName(aName); } - if (aName === "__proto__") aName = "#__proto__"; + //if (aName === "__proto__") aName = "#__proto__"; + aName = sanitizeName(aName, this.options); if (oldVal !== undefined) { if (this.options.trimValues) { oldVal = oldVal.trim(); } - oldVal = this.replaceEntitiesValue(oldVal, tagName, jPath); + oldVal = this.replaceEntitiesValue(oldVal, tagName, this.readonlyMatcher); - // Pass jPath string or matcher based on options.jPath setting - const jPathOrMatcher = this.options.jPath ? jPath.toString() : jPath; + // Pass jPath string or readonlyMatcher based on options.jPath setting + const jPathOrMatcher = this.options.jPath ? jPath.toString() : this.readonlyMatcher; const newVal = this.options.attributeValueProcessor(attrName, oldVal, jPathOrMatcher); if (newVal === null || newVal === undefined) { //don't parse @@ -51760,12 +51937,10 @@ const parseXml = function (xmlData) { } } - if (this.options.transformTagName) { - tagName = this.options.transformTagName(tagName); - } + tagName = transformTagName(this.options.transformTagName, tagName, "", this.options).tagName; if (currentNode) { - textData = this.saveTextToParentTag(textData, currentNode, this.matcher); + textData = this.saveTextToParentTag(textData, currentNode, this.readonlyMatcher); } //check if last tag of nested tag was unpaired tag @@ -51790,7 +51965,7 @@ const parseXml = function (xmlData) { let tagData = readTagExp(xmlData, i, false, "?>"); if (!tagData) throw new Error("Pi Tag is not closed."); - textData = this.saveTextToParentTag(textData, currentNode, this.matcher); + textData = this.saveTextToParentTag(textData, currentNode, this.readonlyMatcher); if ((this.options.ignoreDeclaration && tagData.tagName === "?xml") || this.options.ignorePiTags) ; else { const childNode = new XmlNode(tagData.tagName); @@ -51799,7 +51974,7 @@ const parseXml = function (xmlData) { if (tagData.tagName !== tagData.tagExp && tagData.attrExpPresent) { childNode[":@"] = this.buildAttributesMap(tagData.tagExp, this.matcher, tagData.tagName); } - this.addChild(currentNode, childNode, this.matcher, i); + this.addChild(currentNode, childNode, this.readonlyMatcher, i); } @@ -51809,7 +51984,7 @@ const parseXml = function (xmlData) { if (this.options.commentPropName) { const comment = xmlData.substring(i + 4, endIndex - 2); - textData = this.saveTextToParentTag(textData, currentNode, this.matcher); + textData = this.saveTextToParentTag(textData, currentNode, this.readonlyMatcher); currentNode.add(this.options.commentPropName, [{ [this.options.textNodeName]: comment }]); } @@ -51822,9 +51997,9 @@ const parseXml = function (xmlData) { const closeIndex = findClosingIndex(xmlData, "]]>", i, "CDATA is not closed.") - 2; const tagExp = xmlData.substring(i + 9, closeIndex); - textData = this.saveTextToParentTag(textData, currentNode, this.matcher); + textData = this.saveTextToParentTag(textData, currentNode, this.readonlyMatcher); - let val = this.parseTextData(tagExp, currentNode.tagname, this.matcher, true, false, true, true); + let val = this.parseTextData(tagExp, currentNode.tagname, this.readonlyMatcher, true, false, true, true); if (val == undefined) val = ""; //cdata should be set even if it is 0 length string @@ -51851,18 +52026,13 @@ const parseXml = function (xmlData) { let attrExpPresent = result.attrExpPresent; let closeIndex = result.closeIndex; - if (this.options.transformTagName) { - //console.log(tagExp, tagName) - const newTagName = this.options.transformTagName(tagName); - if (tagExp === tagName) { - tagExp = newTagName; - } - tagName = newTagName; - } + ({ tagName, tagExp } = transformTagName(this.options.transformTagName, tagName, tagExp, this.options)); if (this.options.strictReservedNames && (tagName === this.options.commentPropName || tagName === this.options.cdataPropName + || tagName === this.options.textNodeName + || tagName === this.options.attributesGroupName )) { throw new Error(`Invalid tag name: ${tagName}`); } @@ -51871,7 +52041,7 @@ const parseXml = function (xmlData) { if (currentNode && textData) { if (currentNode.tagname !== '!xml') { //when nested tag is found - textData = this.saveTextToParentTag(textData, currentNode, this.matcher, false); + textData = this.saveTextToParentTag(textData, currentNode, this.readonlyMatcher, false); } } @@ -51960,23 +52130,17 @@ const parseXml = function (xmlData) { this.matcher.pop(); // Pop the stop node tag this.isCurrentNodeStopNode = false; // Reset flag - this.addChild(currentNode, childNode, this.matcher, startIndex); + this.addChild(currentNode, childNode, this.readonlyMatcher, startIndex); } else { //selfClosing tag if (isSelfClosing) { - if (this.options.transformTagName) { - const newTagName = this.options.transformTagName(tagName); - if (tagExp === tagName) { - tagExp = newTagName; - } - tagName = newTagName; - } + ({ tagName, tagExp } = transformTagName(this.options.transformTagName, tagName, tagExp, this.options)); const childNode = new XmlNode(tagName); if (prefixedAttrs) { childNode[":@"] = prefixedAttrs; } - this.addChild(currentNode, childNode, this.matcher, startIndex); + this.addChild(currentNode, childNode, this.readonlyMatcher, startIndex); this.matcher.pop(); // Pop self-closing tag this.isCurrentNodeStopNode = false; // Reset flag } @@ -51985,7 +52149,7 @@ const parseXml = function (xmlData) { if (prefixedAttrs) { childNode[":@"] = prefixedAttrs; } - this.addChild(currentNode, childNode, this.matcher, startIndex); + this.addChild(currentNode, childNode, this.readonlyMatcher, startIndex); this.matcher.pop(); // Pop unpaired tag this.isCurrentNodeStopNode = false; // Reset flag i = result.closeIndex; @@ -52003,7 +52167,7 @@ const parseXml = function (xmlData) { if (prefixedAttrs) { childNode[":@"] = prefixedAttrs; } - this.addChild(currentNode, childNode, this.matcher, startIndex); + this.addChild(currentNode, childNode, this.readonlyMatcher, startIndex); currentNode = childNode; } textData = ""; @@ -52065,7 +52229,7 @@ function replaceEntitiesValue$1(val, tagName, jPath) { } // Replace DOCTYPE entities - for (let entityName in this.docTypeEntities) { + for (const entityName of Object.keys(this.docTypeEntities)) { const entity = this.docTypeEntities[entityName]; const matches = val.match(entity.regx); @@ -52097,19 +52261,38 @@ function replaceEntitiesValue$1(val, tagName, jPath) { } } } - if (val.indexOf('&') === -1) return val; // Early exit - // Replace standard entities - for (let entityName in this.lastEntities) { + for (const entityName of Object.keys(this.lastEntities)) { const entity = this.lastEntities[entityName]; + const matches = val.match(entity.regex); + if (matches) { + this.entityExpansionCount += matches.length; + if (entityConfig.maxTotalExpansions && + this.entityExpansionCount > entityConfig.maxTotalExpansions) { + throw new Error( + `Entity expansion limit exceeded: ${this.entityExpansionCount} > ${entityConfig.maxTotalExpansions}` + ); + } + } val = val.replace(entity.regex, entity.val); } - if (val.indexOf('&') === -1) return val; // Early exit + if (val.indexOf('&') === -1) return val; // Replace HTML entities if enabled if (this.options.htmlEntities) { - for (let entityName in this.htmlEntities) { + for (const entityName of Object.keys(this.htmlEntities)) { const entity = this.htmlEntities[entityName]; + const matches = val.match(entity.regex); + if (matches) { + //console.log(matches); + this.entityExpansionCount += matches.length; + if (entityConfig.maxTotalExpansions && + this.entityExpansionCount > entityConfig.maxTotalExpansions) { + throw new Error( + `Entity expansion limit exceeded: ${this.entityExpansionCount} > ${entityConfig.maxTotalExpansions}` + ); + } + } val = val.replace(entity.regex, entity.val); } } @@ -52306,6 +52489,29 @@ function fromCodePoint(str, base, prefix) { } } +function transformTagName(fn, tagName, tagExp, options) { + if (fn) { + const newTagName = fn(tagName); + if (tagExp === tagName) { + tagExp = newTagName; + } + tagName = newTagName; + } + tagName = sanitizeName(tagName, options); + return { tagName, tagExp }; +} + + + +function sanitizeName(name, options) { + if (criticalProperties.includes(name)) { + throw new Error(`[SECURITY] Invalid name: "${name}" is a reserved JavaScript keyword that could cause prototype pollution`); + } else if (DANGEROUS_PROPERTY_NAMES.includes(name)) { + return options.onDangerousProperty(name); + } + return name; +} + const METADATA_SYMBOL = XmlNode.getMetaDataSymbol(); /** @@ -52338,18 +52544,17 @@ function stripAttributePrefix(attrs, prefix) { * @param {Matcher} matcher - Path matcher instance * @returns */ -function prettify(node, options, matcher) { - return compress(node, options, matcher); +function prettify(node, options, matcher, readonlyMatcher) { + return compress(node, options, matcher, readonlyMatcher); } /** - * * @param {array} arr * @param {object} options * @param {Matcher} matcher - Path matcher instance * @returns object */ -function compress(arr, options, matcher) { +function compress(arr, options, matcher, readonlyMatcher) { let text; const compressedObj = {}; //This is intended to be a plain object for (let i = 0; i < arr.length; i++) { @@ -52372,11 +52577,11 @@ function compress(arr, options, matcher) { continue; } else if (tagObj[property]) { - let val = compress(tagObj[property], options, matcher); + let val = compress(tagObj[property], options, matcher, readonlyMatcher); const isLeaf = isLeafTag(val, options); if (tagObj[":@"]) { - assignAttributes(val, tagObj[":@"], matcher, options); + assignAttributes(val, tagObj[":@"], readonlyMatcher, options); } else if (Object.keys(val).length === 1 && val[options.textNodeName] !== undefined && !options.alwaysCreateTextNode) { val = val[options.textNodeName]; } else if (Object.keys(val).length === 0) { @@ -52398,8 +52603,8 @@ function compress(arr, options, matcher) { //TODO: if a node is not an array, then check if it should be an array //also determine if it is a leaf node - // Pass jPath string or matcher based on options.jPath setting - const jPathOrMatcher = options.jPath ? matcher.toString() : matcher; + // Pass jPath string or readonlyMatcher based on options.jPath setting + const jPathOrMatcher = options.jPath ? readonlyMatcher.toString() : readonlyMatcher; if (options.isArray(property, jPathOrMatcher, isLeaf)) { compressedObj[property] = [val]; } else { @@ -52431,7 +52636,7 @@ function propName$1(obj) { } } -function assignAttributes(obj, attrMap, matcher, options) { +function assignAttributes(obj, attrMap, readonlyMatcher, options) { if (attrMap) { const keys = Object.keys(attrMap); const len = keys.length; //don't make it inline @@ -52446,8 +52651,8 @@ function assignAttributes(obj, attrMap, matcher, options) { // For attributes, we need to create a temporary path // Pass jPath string or matcher based on options.jPath setting const jPathOrMatcher = options.jPath - ? matcher.toString() + "." + rawAttrName - : matcher; + ? readonlyMatcher.toString() + "." + rawAttrName + : readonlyMatcher; if (options.isArray(atrrName, jPathOrMatcher, true, true)) { obj[atrrName] = [attrMap[atrrName]]; @@ -52507,7 +52712,7 @@ class XMLParser { orderedObjParser.addExternalEntities(this.externalEntities); const orderedResult = orderedObjParser.parseXml(xmlData); if (this.options.preserveOrder || orderedResult === undefined) return orderedResult; - else return prettify(orderedResult, this.options, orderedObjParser.matcher); + else return prettify(orderedResult, this.options, orderedObjParser.matcher, orderedObjParser.readonlyMatcher); } /** @@ -52579,6 +52784,9 @@ function arrToStr(arr, options, indentation, matcher, stopNodeExpressions) { let xmlStr = ""; let isPreviousElementTag = false; + if (options.maxNestedTags && matcher.getDepth() > options.maxNestedTags) { + throw new Error("Maximum nested tags exceeded"); + } if (!Array.isArray(arr)) { // Non-array values (e.g. string tag values) should be treated as text content @@ -52655,6 +52863,7 @@ function arrToStr(arr, options, indentation, matcher, stopNodeExpressions) { if (isStopNode) { tagValue = getRawContent(tagObj[tagName], options); } else { + tagValue = arrToStr(tagObj[tagName], options, newIdentation, matcher, stopNodeExpressions); } @@ -52880,6 +53089,7 @@ const defaultOptions = { // transformTagName: false, // transformAttributeName: false, oneListGroup: false, + maxNestedTags: 100, jPath: true // When true, callbacks receive string jPath; when false, receive Matcher instance }; @@ -52955,7 +53165,9 @@ Builder.prototype.build = function (jObj) { Builder.prototype.j2x = function (jObj, level, matcher) { let attrStr = ''; let val = ''; - + if (this.options.maxNestedTags && matcher.getDepth() >= this.options.maxNestedTags) { + throw new Error("Maximum nested tags exceeded"); + } // Get jPath based on option: string for backward compatibility, or Matcher for new features const jPath = this.options.jPath ? matcher.toString() : matcher; diff --git a/package-lock.json b/package-lock.json index 90074f4..3f6e4f9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "setup-graalvm", - "version": "1.5.0", + "version": "1.5.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "setup-graalvm", - "version": "1.5.0", + "version": "1.5.1", "license": "UPL", "dependencies": { "@actions/cache": "^6.0.0", @@ -128,10 +128,9 @@ } }, "node_modules/@actions/glob/node_modules/brace-expansion": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", - "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", - "license": "MIT", + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.3.tgz", + "integrity": "sha512-MCV/fYJEbqx68aE58kv2cA/kiky1G8vux3OR6/jbS+jIMe/6fJWa0DTzJU7dqijOWYwHi1t29FlfYI9uytqlpA==", "dependencies": { "balanced-match": "^1.0.0" } @@ -2797,11 +2796,10 @@ } }, "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { - "version": "5.0.4", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.4.tgz", - "integrity": "sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg==", + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz", + "integrity": "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==", "dev": true, - "license": "MIT", "dependencies": { "balanced-match": "^4.0.2" }, @@ -3292,11 +3290,10 @@ } }, "node_modules/anymatch/node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", "dev": true, - "license": "MIT", "engines": { "node": ">=8.6" }, @@ -3591,11 +3588,10 @@ "license": "Apache-2.0" }, "node_modules/brace-expansion": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.13.tgz", + "integrity": "sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==", "dev": true, - "license": "MIT", "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -5121,35 +5117,33 @@ "license": "MIT" }, "node_modules/fast-xml-builder": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/fast-xml-builder/-/fast-xml-builder-1.1.2.tgz", - "integrity": "sha512-NJAmiuVaJEjVa7TjLZKlYd7RqmzOC91EtPFXHvlTcqBVo50Qh7XV5IwvXi1c7NRz2Q/majGX9YLcwJtWgHjtkA==", + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/fast-xml-builder/-/fast-xml-builder-1.1.4.tgz", + "integrity": "sha512-f2jhpN4Eccy0/Uz9csxh3Nu6q4ErKxf0XIsasomfOihuSUa3/xw6w8dnOtCDgEItQFJG8KyXPzQXzcODDrrbOg==", "funding": [ { "type": "github", "url": "https://github.com/sponsors/NaturalIntelligence" } ], - "license": "MIT", "dependencies": { "path-expression-matcher": "^1.1.3" } }, "node_modules/fast-xml-parser": { - "version": "5.5.3", - "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.5.3.tgz", - "integrity": "sha512-Ymnuefk6VzAhT3SxLzVUw+nMio/wB1NGypHkgetwtXcK1JfryaHk4DWQFGVwQ9XgzyS5iRZ7C2ZGI4AMsdMZ6A==", + "version": "5.5.9", + "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.5.9.tgz", + "integrity": "sha512-jldvxr1MC6rtiZKgrFnDSvT8xuH+eJqxqOBThUVjYrxssYTo1avZLGql5l0a0BAERR01CadYzZ83kVEkbyDg+g==", "funding": [ { "type": "github", "url": "https://github.com/sponsors/NaturalIntelligence" } ], - "license": "MIT", "dependencies": { - "fast-xml-builder": "^1.1.2", - "path-expression-matcher": "^1.1.3", - "strnum": "^2.1.2" + "fast-xml-builder": "^1.1.4", + "path-expression-matcher": "^1.2.0", + "strnum": "^2.2.2" }, "bin": { "fxparser": "src/cli/cli.js" @@ -5241,11 +5235,10 @@ } }, "node_modules/flatted": { - "version": "3.4.1", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.1.tgz", - "integrity": "sha512-IxfVbRFVlV8V/yRaGzk0UVIcsKKHMSfYw66T/u4nTwlWteQePsxe//LjudR1AMX4tZW3WFCh3Zqa/sjlqpbURQ==", - "dev": true, - "license": "ISC" + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz", + "integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==", + "dev": true }, "node_modules/for-each": { "version": "0.3.5", @@ -5502,11 +5495,10 @@ } }, "node_modules/glob/node_modules/brace-expansion": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", - "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.3.tgz", + "integrity": "sha512-MCV/fYJEbqx68aE58kv2cA/kiky1G8vux3OR6/jbS+jIMe/6fJWa0DTzJU7dqijOWYwHi1t29FlfYI9uytqlpA==", "dev": true, - "license": "MIT", "dependencies": { "balanced-match": "^1.0.0" } @@ -5585,11 +5577,10 @@ "license": "MIT" }, "node_modules/handlebars": { - "version": "4.7.8", - "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.8.tgz", - "integrity": "sha512-vafaFqs8MZkRrSX7sFVUdo3ap/eNiLnb4IakshzvP56X5Nr1iGKAIqdX6tMlm6HcNRIkr6AxO5jFEoJzzpT8aQ==", + "version": "4.7.9", + "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.9.tgz", + "integrity": "sha512-4E71E0rpOaQuJR2A3xDZ+GM1HyWYv1clR58tC8emQNeQe3RH7MAzSbat+V0wG78LQBo6m6bzSG/L4pBuCsgnUQ==", "dev": true, - "license": "MIT", "dependencies": { "minimist": "^1.2.5", "neo-async": "^2.6.2", @@ -7256,11 +7247,10 @@ } }, "node_modules/micromatch/node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", "dev": true, - "license": "MIT", "engines": { "node": ">=8.6" }, @@ -7635,16 +7625,15 @@ } }, "node_modules/path-expression-matcher": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/path-expression-matcher/-/path-expression-matcher-1.1.3.tgz", - "integrity": "sha512-qdVgY8KXmVdJZRSS1JdEPOKPdTiEK/pi0RkcT2sw1RhXxohdujUlJFPuS1TSkevZ9vzd3ZlL7ULl1MHGTApKzQ==", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/path-expression-matcher/-/path-expression-matcher-1.2.0.tgz", + "integrity": "sha512-DwmPWeFn+tq7TiyJ2CxezCAirXjFxvaiD03npak3cRjlP9+OjTmSy1EpIrEbh+l6JgUundniloMLDQ/6VTdhLQ==", "funding": [ { "type": "github", "url": "https://github.com/sponsors/NaturalIntelligence" } ], - "license": "MIT", "engines": { "node": ">=14.0.0" } @@ -7708,11 +7697,10 @@ "license": "ISC" }, "node_modules/picomatch": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", - "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", "dev": true, - "license": "MIT", "engines": { "node": ">=12" }, @@ -8643,16 +8631,15 @@ } }, "node_modules/strnum": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/strnum/-/strnum-2.2.0.tgz", - "integrity": "sha512-Y7Bj8XyJxnPAORMZj/xltsfo55uOiyHcU2tnAVzHUnSJR/KsEX+9RoDeXEnsXtl/CX4fAcrt64gZ13aGaWPeBg==", + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/strnum/-/strnum-2.2.2.tgz", + "integrity": "sha512-DnR90I+jtXNSTXWdwrEy9FakW7UX+qUZg28gj5fk2vxxl7uS/3bpI4fjFYVmdK9etptYBPNkpahuQnEwhwECqA==", "funding": [ { "type": "github", "url": "https://github.com/sponsors/NaturalIntelligence" } - ], - "license": "MIT" + ] }, "node_modules/supports-color": { "version": "7.2.0", @@ -9036,11 +9023,10 @@ } }, "node_modules/ts-jest-resolver/node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", "dev": true, - "license": "MIT", "engines": { "node": ">=8.6" }, diff --git a/package.json b/package.json index 2650e93..6ecdb4c 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "setup-graalvm", "author": "GraalVM Community", "description": "GitHub Action for GraalVM", - "version": "1.5.0", + "version": "1.5.1", "type": "module", "private": true, "repository": { diff --git a/src/constants.ts b/src/constants.ts index 20d1d0c..23e1d6e 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -1,6 +1,6 @@ import * as otypes from '@octokit/types' -export const ACTION_VERSION = '1.5.0' +export const ACTION_VERSION = '1.5.1' export const INPUT_VERSION = 'version' export const INPUT_GDS_TOKEN = 'gds-token'