VYPR
Moderate severityNVD Advisory· Published Mar 24, 2026· Updated Mar 25, 2026

fast-xml-parser: Entity Expansion Limits Bypassed When Set to Zero Due to JavaScript Falsy Evaluation

CVE-2026-33349

Description

fast-xml-parser allows users to process XML from JS object without C/C++ based libraries or callbacks. From version 4.0.0-beta.3 to before version 5.5.7, the DocTypeReader in fast-xml-parser uses JavaScript truthy checks to evaluate maxEntityCount and maxEntitySize configuration limits. When a developer explicitly sets either limit to 0 — intending to disallow all entities or restrict entity size to zero bytes — the falsy nature of 0 in JavaScript causes the guard conditions to short-circuit, completely bypassing the limits. An attacker who can supply XML input to such an application can trigger unbounded entity expansion, leading to memory exhaustion and denial of service. This issue has been patched in version 5.5.7.

Affected packages

Versions sourced from the GitHub Security Advisory.

PackageAffected versionsPatched versions
fast-xml-parsernpm
>= 4.0.0-beta.3, < 4.5.54.5.5
fast-xml-parsernpm
>= 5.0.0, < 5.5.75.5.7

Affected products

1

Patches

2
88d0936a23da

apply all fixes from v5

https://github.com/NaturalIntelligence/fast-xml-parseramit kumar guptaMar 22, 2026via ghsa
5 files changed · +157 19
  • src/fxp.d.ts+24 0 modified
    @@ -34,6 +34,13 @@ export type ProcessEntitiesOptions = {
        */
       maxExpandedLength?: number;
     
    +  /**
    +   * Maximum number of entities allowed in the XML
    +   * 
    +   * Defaults to `100`
    +   */
    +  maxEntityCount?: number;
    +
       /**
        * Array of tag names where entity replacement is allowed.
        * If null, entities are replaced in all tags.
    @@ -292,6 +299,16 @@ export type X2jOptions = {
        * Defaults to `true`
        */
       strictReservedNames?: boolean;
    +
    +  /**
    +   * Function to sanitize dangerous property names
    +   * 
    +   * @param name - The name of the property
    +   * @returns {string} The sanitized name
    +   * 
    +   * Defaults to `(name) => __name`
    +   */
    +  onDangerousProperty?: (name: string) => string;
     };
     
     
    @@ -469,6 +486,13 @@ export type XmlBuilderOptions = {
     
     
       oneListGroup?: boolean;
    +
    +  /**
    + * Maximum number of nested tags
    + * 
    + * Defaults to `100`
    + */
    +  maxNestedTags?: number;
     };
     
     type ESchema = string | object | Array<string | object>;
    
  • src/util.js+26 9 modified
    @@ -5,7 +5,7 @@ const nameChar = nameStartChar + '\\-.\\d\\u00B7\\u0300-\\u036F\\u203F-\\u2040';
     const nameRegexp = '[' + nameStartChar + '][' + nameChar + ']*'
     const regexName = new RegExp('^' + nameRegexp + '$');
     
    -const getAllMatches = function(string, regex) {
    +const getAllMatches = function (string, regex) {
       const matches = [];
       let match = regex.exec(string);
       while (match) {
    @@ -21,16 +21,16 @@ const getAllMatches = function(string, regex) {
       return matches;
     };
     
    -const isName = function(string) {
    +const isName = function (string) {
       const match = regexName.exec(string);
       return !(match === null || typeof match === 'undefined');
     };
     
    -exports.isExist = function(v) {
    +exports.isExist = function (v) {
       return typeof v !== 'undefined';
     };
     
    -exports.isEmptyObject = function(obj) {
    +exports.isEmptyObject = function (obj) {
       return Object.keys(obj).length === 0;
     };
     
    @@ -39,13 +39,13 @@ exports.isEmptyObject = function(obj) {
      * @param {*} target
      * @param {*} a
      */
    -exports.merge = function(target, a, arrayMode) {
    +exports.merge = function (target, a, arrayMode) {
       if (a) {
         const keys = Object.keys(a); // will return an array of own properties
         const len = keys.length; //don't make it inline
         for (let i = 0; i < len; i++) {
           if (arrayMode === 'strict') {
    -        target[keys[i]] = [ a[keys[i]] ];
    +        target[keys[i]] = [a[keys[i]]];
           } else {
             target[keys[i]] = a[keys[i]];
           }
    @@ -56,17 +56,34 @@ exports.merge = function(target, a, arrayMode) {
       return Object.assign(b,a);
     } */
     
    -exports.getValue = function(v) {
    +exports.getValue = function (v) {
       if (exports.isExist(v)) {
         return v;
       } else {
         return '';
       }
     };
     
    -// const fakeCall = function(a) {return a;};
    -// const fakeCallNoReturn = function() {};
    +/**
    + * 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"];
     
     exports.isName = isName;
     exports.getAllMatches = getAllMatches;
     exports.nameRegexp = nameRegexp;
    +exports.DANGEROUS_PROPERTY_NAMES = DANGEROUS_PROPERTY_NAMES;
    +exports.criticalProperties = criticalProperties;
    
  • src/xmlparser/DocTypeReader.js+12 2 modified
    @@ -8,6 +8,7 @@ class DocTypeReader {
     
         readDocType(xmlData, i) {
             const entities = Object.create(null);
    +        let entityCount = 0;
     
             if (xmlData[i + 3] === 'O' &&
                 xmlData[i + 4] === 'C' &&
    @@ -28,11 +29,20 @@ class DocTypeReader {
                             let entityName, val;
                             [entityName, val, i] = this.readEntityExp(xmlData, i + 1, this.suppressValidationErr);
                             if (val.indexOf("&") === -1) { //Parameter entities are not supported
    -                            const escaped = entityName.replace(/[.\-+*:]/g, '\\.');
    +                            if (this.options.enabled !== false &&
    +                                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, '\\$&');
                                 entities[entityName] = {
                                     regx: RegExp(`&${escaped};`, "g"),
                                     val: val
                                 };
    +                            entityCount++;
                             }
                         } else if (hasBody && hasSeq(xmlData, "!ELEMENT", i)) {
                             i += 8; //Not supported
    @@ -122,7 +132,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})`
    
  • src/xmlparser/OptionsBuilder.js+59 5 modified
    @@ -1,4 +1,12 @@
     
    +const { DANGEROUS_PROPERTY_NAMES, criticalProperties } = require("../util");
    +
    +const defaultOnDangerousProperty = (name) => {
    +  if (DANGEROUS_PROPERTY_NAMES.includes(name)) {
    +    return "__" + name;
    +  }
    +  return name;
    +};
     const defaultOptions = {
       preserveOrder: false,
       attributeNamePrefix: '@_',
    @@ -41,7 +49,32 @@ const defaultOptions = {
       captureMetaData: false,
       maxNestedTags: 100,
       strictReservedNames: true,
    +  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
    @@ -65,11 +98,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,
    +      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
         };
    @@ -82,6 +116,26 @@ function normalizeProcessEntities(value) {
     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);
       //console.debug(built.processEntities)
    
  • src/xmlparser/OrderedObjParser.js+36 3 modified
    @@ -162,7 +162,7 @@ function buildAttributesMap(attrStr, jPath, tagName) {
             if (this.options.transformAttributeName) {
               aName = this.options.transformAttributeName(aName);
             }
    -        if (aName === "__proto__") aName = "#__proto__";
    +        aName = sanitizeName(aName, this.options);
             if (oldVal !== undefined) {
               if (this.options.trimValues) {
                 oldVal = oldVal.trim();
    @@ -325,6 +325,8 @@ const parseXml = function (xmlData) {
             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}`);
             }
    @@ -523,16 +525,37 @@ const replaceEntitiesValue = function (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
     
       // 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);
         }
       }
    @@ -725,4 +748,14 @@ function fromCodePoint(str, base, prefix) {
       }
     }
     
    +function sanitizeName(name, options) {
    +  if (util.criticalProperties.includes(name)) {
    +    throw new Error(`[SECURITY] Invalid name: "${name}" is a reserved JavaScript keyword that could cause prototype pollution`);
    +  } else if (util.DANGEROUS_PROPERTY_NAMES.includes(name)) {
    +    return options.onDangerousProperty(name);
    +  }
    +  return name;
    +}
    +
     module.exports = OrderedObjParser;
    +
    
239b64aa1fc5

check for min value for entity exapantion options

https://github.com/NaturalIntelligence/fast-xml-parseramit kumar guptaMar 19, 2026via ghsa
2 files changed · +8 8
  • src/xmlparser/DocTypeReader.js+2 2 modified
    @@ -28,7 +28,7 @@ export default 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})`
    @@ -126,7 +126,7 @@ export default 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})`
    
  • src/xmlparser/OptionsBuilder.js+6 6 modified
    @@ -103,12 +103,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
         };
    

Vulnerability mechanics

Generated by null/stub on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.

References

5

News mentions

0

No linked articles in our index yet.