CVE-2023-35167: Improper Access Control (CWE-284)
value: Remult is a CRUD framework for full-stack TypeScript. If you used the apiPrefilter option of the `@Entity` decorator, by setting it to a function that returns a filter that prevents unauthorized access to data, an attacker who knows the `id` of an entity instance is not authorized to access, can gain read, update and delete access to it. The issue is fixed in version 0.20.6. As a workaround, set the `apiPrefilter` option to a filter object instead of a function.
CWE description: The product does not restrict or incorrectly restricts access to a resource from an unauthorized actor.
CVSS v3 base score: 6.3
Published: 2023-06-23T20:15Z
Repository: https://github.com/remult/remult
Language: TypeScript
Files changed: CHANGELOG.md, misc/dts-compare/remult-mongo.d.ts, misc/dts-compare/src/shared-tests/db-tests-setup.d.ts, projects/core/src/data-api.ts, projects/core/src/tests/test-data-api/predefined-filter.spec.ts, projects/test-angular/src/app/products-test/products.component.ts
Commit date: 2023-06-19 18:50:12 +0300
Commit message: fixed api prefilter

Diff: diff --git a/CHANGELOG.md b/CHANGELOG.md
index 012cc85f..2a9dda84 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,9 +1,13 @@
 # Changelog
 
 All notable changes to this project will be documented in this file.
 
+## [0.20.6] - 2023-06-19
+
+- Fixed an issue when apiPrefilter was an arrow function, it did not affect get of a specific resource
+
 ## [0.20.5] - 2023-06-11
 
 - fixed issue #200 transactions on mongo db
 
 ## [0.20.4] - 2023-06-05
diff --git a/misc/dts-compare/remult-mongo.d.ts b/misc/dts-compare/remult-mongo.d.ts
index 135a8318..de658cd9 100644
--- a/misc/dts-compare/remult-mongo.d.ts
+++ b/misc/dts-compare/remult-mongo.d.ts
@@ -1,13 +1,21 @@
-import type { MongoClient, Db } from 'mongodb';
+import type { MongoClient, Db, ClientSession } from 'mongodb';
 import { DataProvider, EntityDataProvider, EntityFilter, EntityMetadata, Remult } from '.';
 import { RepositoryOverloads } from './src/remult3';
 export declare class MongoDataProvider implements DataProvider {
     private db;
     private client;
-    constructor(db: Db, client: MongoClient);
-    static getDb(remult?: Remult): Db;
+    constructor(db: Db, client: MongoClient, options?: {
+        session?: ClientSession;
+        disableTransactions?: boolean;
+    });
+    session?: ClientSession;
+    disableTransactions: boolean;
+    static getDb(remult?: Remult): {
+        db: Db;
+        session: ClientSession;
+    };
     getEntityDataProvider(entity: EntityMetadata<any>): EntityDataProvider;
     transaction(action: (dataProvider: DataProvider) => Promise<void>): Promise<void>;
     static filterToRaw<entityType>(entity: RepositoryOverloads<entityType>, condition: EntityFilter<entityType>): Promise<{
         $and: any[];
     } | {
diff --git a/misc/dts-compare/src/shared-tests/db-tests-setup.d.ts b/misc/dts-compare/src/shared-tests/db-tests-setup.d.ts
index 2f2db96d..789e6b16 100644
--- a/misc/dts-compare/src/shared-tests/db-tests-setup.d.ts
+++ b/misc/dts-compare/src/shared-tests/db-tests-setup.d.ts
@@ -11,10 +11,11 @@ export declare function testAll(key: string, what: dbTestWhatSignature, focus?:
 export declare function addDatabaseToTest(tester: dbTestMethodSignature, key?: string): dbTestMethodSignature;
 export declare function testInMemory(key: string, what: dbTestWhatSignature, focus?: boolean): void;
 export declare const TestDbs: {
     restDataProvider: string;
     mongo: string;
+    inMemory: string;
 };
 export declare type dbTestWhatSignature = ((db: {
     db: DataProvider;
     remult: Remult;
     createEntity<entityType>(entity: ClassType<entityType>): Promise<Repository<entityType>>;
diff --git a/projects/core/src/data-api.ts b/projects/core/src/data-api.ts
index 7782ba40..633cd952 100644
--- a/projects/core/src/data-api.ts
+++ b/projects/core/src/data-api.ts
@@ -193,14 +193,20 @@ export class DataApi<T = any> {
 
 
   private async doOnId(response: DataApiResponse, id: any, what: (row: T) => Promise<void>) {
     try {
 
-
+      var where: EntityFilter<any>[] = [this.repository.metadata.idMetadata.getIdFilter(id)];
+      if (this.repository.metadata.options.apiPrefilter) {
+        if (typeof this.repository.metadata.options.apiPrefilter === "function")
+          where.push(await this.repository.metadata.options.apiPrefilter());
+        else
+          where.push(this.repository.metadata.options.apiPrefilter);
+      }
 
       await this.repository.find({
-        where: { $and: [this.repository.metadata.options.apiPrefilter, this.repository.metadata.idMetadata.getIdFilter(id)] } as EntityFilter<any>
+        where: { $and: where } as EntityFilter<any>
       })
         .then(async r => {
           if (r.length == 0)
             response.notFound();
           else if (r.length > 1)
diff --git a/projects/core/src/tests/test-data-api/predefined-filter.spec.ts b/projects/core/src/tests/test-data-api/predefined-filter.spec.ts
index fa54dc63..39a69828 100644
--- a/projects/core/src/tests/test-data-api/predefined-filter.spec.ts
+++ b/projects/core/src/tests/test-data-api/predefined-filter.spec.ts
@@ -129,10 +129,25 @@ describe("data api", () => {
       d.ok();
     };
     await api.put(t, 1, { name: 'YAEL' });
     d.test();
   });
+  it("put id 1 works with predefined filterand shouldnt return anything", async () => {
+    let [c, remult] = await createData(async (i) => {
+      await i(1, 'noam', 'a');
+      await i(2, 'yael', 'b');
+      await i(3, 'yoni', 'a');
+    }, CategoriesForThisTest2);
+    var api = new DataApi(c, remult);
+    let t = new TestDataApiResponse();
+    let d = new Done();
+    t.notFound = () => {
+      d.ok();
+    };
+    await api.put(t, 1, { name: 'YAEL' });
+    d.test();
+  });
   it("getArray works with predefined filter", async () => {
     let [c, remult] = await createData(async (i) => {
       await i(1, 'noam', 'a');
       await i(2, 'yael', 'b');
       await i(3, 'yoni', 'a');
diff --git a/projects/test-angular/src/app/products-test/products.component.ts b/projects/test-angular/src/app/products-test/products.component.ts
index 9fdc7ce0..ce271f10 100644
--- a/projects/test-angular/src/app/products-test/products.component.ts
+++ b/projects/test-angular/src/app/products-test/products.component.ts
@@ -40,11 +40,15 @@ export class ProductsComponent {
 
 
 
 }
 
-@Entity("categories", { allowApiRead: false })
+@Entity<Category>("categories", {
+  allowApiCrud: true, apiPrefilter: () => ({
+    id: { $ne: "clj30u9o500000kr3956ph9ep" }
+  })
+})
 export class Category {
   @Fields.cuid()
   id = ''
   @Fields.string()
   name = ''

Vulnerable code: import type { MongoClient, Db } from 'mongodb';
    constructor(db: Db, client: MongoClient);
    static getDb(remult?: Remult): Db;

        where: { $and: [this.repository.metadata.options.apiPrefilter, this.repository.metadata.idMetadata.getIdFilter(id)] } as EntityFilter<any>
@Entity("categories", { allowApiRead: false })
Fixed code: ## [0.20.6] - 2023-06-19

- Fixed an issue when apiPrefilter was an arrow function, it did not affect get of a specific resource

import type { MongoClient, Db, ClientSession } from 'mongodb';
    constructor(db: Db, client: MongoClient, options?: {
        session?: ClientSession;
        disableTransactions?: boolean;
    });
    session?: ClientSession;
    disableTransactions: boolean;
    static getDb(remult?: Remult): {
        db: Db;
        session: ClientSession;
    };
    inMemory: string;
      var where: EntityFilter<any>[] = [this.repository.metadata.idMetadata.getIdFilter(id)];
      if (this.repository.metadata.options.apiPrefilter) {
        if (typeof this.repository.metadata.options.apiPrefilter === "function")
          where.push(await this.repository.metadata.options.apiPrefilter());
        else
          where.push(this.repository.metadata.options.apiPrefilter);
      }
        where: { $and: where } as EntityFilter<any>
  it("put id 1 works with predefined filterand shouldnt return anything", async () => {
    let [c, remult] = await createData(async (i) => {
      await i(1, 'noam', 'a');
      await i(2, 'yael', 'b');
      await i(3, 'yoni', 'a');
    }, CategoriesForThisTest2);
    var api = new DataApi(c, remult);
    let t = new TestDataApiResponse();
    let d = new Done();
    t.notFound = () => {
      d.ok();
    };
    await api.put(t, 1, { name: 'YAEL' });
    d.test();
  });
@Entity<Category>("categories", {
  allowApiCrud: true, apiPrefilter: () => ({
    id: { $ne: "clj30u9o500000kr3956ph9ep" }
  })
})