VYPR
Critical severity9.8NVD Advisory· Published Feb 10, 2026· Updated Apr 15, 2026

CVE-2026-1774

CVE-2026-1774

Description

CASL Ability, versions 2.4.0 through 6.7.4, contains a prototype pollution vulnerability.

Affected packages

Versions sourced from the GitHub Security Advisory.

PackageAffected versionsPatched versions
@casl/abilitynpm
>= 2.4.0, < 6.7.56.7.5

Affected products

1

Patches

1
39da920ec1df

fix: ignores potentially insecure fields in rulesToFields (#1093)

https://github.com/stalniy/caslSerhii StotskyiDec 17, 2025via ghsa
2 files changed · +32 14
  • packages/casl-ability/spec/rulesToFields.spec.ts+21 10 renamed
    @@ -1,12 +1,11 @@
     import { rulesToFields } from '../src/extra'
    -import { defineAbility, PureAbility } from '../src'
    -import './spec_helper'
    +import { createMongoAbility, defineAbility } from '../src'
     
    -describe('rulesToFields', () => {
    +describe(rulesToFields.name, () => {
       it('returns an empty object for an empty `Ability` instance', () => {
    -    const object = rulesToFields(new PureAbility(), 'read', 'Post')
    +    const object = rulesToFields(createMongoAbility(), 'read', 'Post')
     
    -    expect(object).to.be.an('object').and.empty
    +    expect(Object.keys(object)).toHaveLength(0)
       })
     
       it('returns an empty object if `Ability` contains only inverted rules', () => {
    @@ -16,14 +15,14 @@ describe('rulesToFields', () => {
         })
         const object = rulesToFields(ability, 'read', 'Post')
     
    -    expect(object).to.be.an('object').and.empty
    +    expect(Object.keys(object)).toHaveLength(0)
       })
     
       it('returns an empty object for `Ability` instance with rules without conditions', () => {
         const ability = defineAbility(can => can('read', 'Post'))
         const object = rulesToFields(ability, 'read', 'Post')
     
    -    expect(object).to.be.an('object').and.empty
    +    expect(Object.keys(object)).toHaveLength(0)
       })
     
       it('extracts field values from direct rule conditions', () => {
    @@ -33,7 +32,7 @@ describe('rulesToFields', () => {
         })
         const object = rulesToFields(ability, 'read', 'Post')
     
    -    expect(object).to.deep.equal({ id: 5, private: true })
    +    expect(object).toEqual({ id: 5, private: true })
       })
     
       it('correctly sets values for fields declared with `dot notation`', () => {
    @@ -43,7 +42,7 @@ describe('rulesToFields', () => {
         })
         const object = rulesToFields(ability, 'read', 'Post')
     
    -    expect(object).to.deep.equal({
    +    expect(object).toEqual({
           id: 5,
           state: {
             private: true
    @@ -58,6 +57,18 @@ describe('rulesToFields', () => {
         })
         const object = rulesToFields(ability, 'read', 'Post')
     
    -    expect(object).to.deep.equal({ private: true })
    +    expect(object).toEqual({ private: true })
    +  })
    +
    +  it('skips forbidden properties', () => {
    +    const ability = defineAbility((can) => {
    +      can('read', 'Post', { '__proto__.__pollutedValue__': 1 })
    +      can('read', 'Post', { constructor: 1 })
    +      can('read', 'Post', { prototype: 2 })
    +    })
    +    const object = rulesToFields(ability, 'read', 'Post')
    +
    +    expect(({} as any).__pollutedValue__).toBeUndefined()
    +    expect(Object.keys(object)).toEqual(['__pollutedValue__'])
       })
     })
    
  • packages/casl-ability/src/utils.ts+11 4 modified
    @@ -1,9 +1,13 @@
     import { AnyObject, Subject, SubjectType, SubjectClass, ForcedSubject, AliasesMap } from './types';
     
    +const hasOwn: (o: object, v: PropertyKey) => boolean = Object.hasOwn ||
    +  ((obj, prop) => Object.prototype.hasOwnProperty.call(obj, prop));
    +
     export function wrapArray<T>(value: T[] | T): T[] {
       return Array.isArray(value) ? value : [value];
     }
     
    +const FORBIDDEN_PROPERTIES = new Set(['__proto__', 'constructor', 'prototype']);
     export function setByPath(object: AnyObject, path: string, value: unknown): void {
       let ref = object;
       let lastKey = path;
    @@ -13,12 +17,15 @@ export function setByPath(object: AnyObject, path: string, value: unknown): void
     
         lastKey = keys.pop()!;
         ref = keys.reduce((res, prop) => {
    +      if (FORBIDDEN_PROPERTIES.has(prop)) return res;
           res[prop] = res[prop] || {};
           return res[prop] as AnyObject;
         }, object);
       }
     
    -  ref[lastKey] = value;
    +  if (!FORBIDDEN_PROPERTIES.has(lastKey)) {
    +    ref[lastKey] = value;
    +  }
     }
     
     const TYPE_FIELD = '__caslSubjectType__';
    @@ -27,7 +34,7 @@ export function setSubjectType<
       U extends Record<PropertyKey, any>
     >(type: T, object: U): U & ForcedSubject<T> {
       if (object) {
    -    if (!Object.hasOwn(object, TYPE_FIELD)) {
    +    if (!hasOwn(object, TYPE_FIELD)) {
           Object.defineProperty(object, TYPE_FIELD, { value: type });
         } else if (type !== object[TYPE_FIELD]) {
           throw new Error(`Trying to cast object to subject type ${type} but previously it was casted to ${object[TYPE_FIELD]}`);
    @@ -48,7 +55,7 @@ export function getSubjectTypeName(value: SubjectType) {
     }
     
     export function detectSubjectType(object: Exclude<Subject, SubjectType>): string {
    -  if (Object.hasOwn(object, TYPE_FIELD)) {
    +  if (hasOwn(object, TYPE_FIELD)) {
         return object[TYPE_FIELD];
       }
     
    @@ -68,7 +75,7 @@ function expandActions(aliasMap: AliasesMap, rawActions: string | string[], merg
       while (i < actions.length) {
         const action = actions[i++];
     
    -    if (Object.hasOwn(aliasMap, action)) {
    +    if (hasOwn(aliasMap, action)) {
           actions = merge(actions, aliasMap[action]);
         }
       }
    

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

8

News mentions

0

No linked articles in our index yet.