Security advisory GHSA-38cw-85xc-xr9x: Veramo is Vulnerable to SQL Injection in Veramo Data Store ORM
Package: @veramo/data-store (npm), vulnerable version: 6.0.1, fixed in: 6.0.2
CWE: CWE-89
Files changed by the fix:
packages/core-types/src/types/IDataStoreORM.ts
packages/data-store-json/src/data-store-json.ts
packages/data-store/src/data-store-orm.ts

Fix diff:
diff --git a/__tests__/shared/saveClaims.ts b/__tests__/shared/saveClaims.ts
index 0e4d00927..908261c1e 100644
--- a/__tests__/shared/saveClaims.ts
+++ b/__tests__/shared/saveClaims.ts
@@ -106,6 +106,30 @@ export default (testContext: {
       expect(count).toEqual(credentials.length)
     })
 
+    it('blocks injection attempts', async () => {
+      expect.assertions(1);
+      const query = agent.dataStoreORMGetVerifiableCredentialsByClaims({
+        where: [
+          {
+            value: ['string'],
+            not: true,
+            op: 'zx' as any,
+            column: 'isObj',
+          },
+        ],
+        skip: 0,
+        take: 0,
+        order: [
+          {
+            direction: 'ASC',
+            column:
+              'issuanceDate" AS "issuanceDate" FROM "claim" "claim" LEFT JOIN "identifier" "issuer" ON "issuer"."did"="claim"."issuerDid"  LEFT JOIN "identifier" "subject" ON "subject"."did"="claim"."subjectDid"  LEFT JOIN "credential" "credential" ON "credential"."hash"="claim"."credentialHash" where not(claim.isObj in (?)) and 1=0 UNION ALL SELECT 1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,(SELECT json_object(\'alias\', alias, \'type\', type, \'privateKeyHex\', privateKeyHex) ),22,23,24,25,26,27,28,29 from `private-key`-- -' as any,
+          },
+        ],
+      })
+      await expect(query).rejects.toThrow(/Invalid column name/);
+    })
+
     it('should be able to find all the credentials when query by claim type and value', async () => {
       const credentials = await agent.dataStoreORMGetVerifiableCredentialsByClaims({
         where: [
@@ -192,7 +216,7 @@ export default (testContext: {
 
       expect(credentialsAllDesc[0].verifiableCredential.issuanceDate).toEqual(credentials1[0].verifiableCredential.issuanceDate)
       expect(credentialsAllDesc[1].verifiableCredential.issuanceDate).toEqual(credentials2[0].verifiableCredential.issuanceDate)
-      
+
       expect(credentialsAllDesc[0].verifiableCredential.issuanceDate).toEqual(credentialsAllAsc[2].verifiableCredential.issuanceDate)
       expect(credentialsAllDesc[1].verifiableCredential.issuanceDate).toEqual(credentialsAllAsc[1].verifiableCredential.issuanceDate)
       expect(credentialsAllDesc[2].verifiableCredential.issuanceDate).toEqual(credentialsAllAsc[0].verifiableCredential.issuanceDate)
diff --git a/packages/core-types/src/types/IDataStoreORM.ts b/packages/core-types/src/types/IDataStoreORM.ts
index cabc672a7..b6fcd9714 100644
--- a/packages/core-types/src/types/IDataStoreORM.ts
+++ b/packages/core-types/src/types/IDataStoreORM.ts
@@ -3,6 +3,29 @@ import { IIdentifier } from './IIdentifier.js'
 import { IAgentContext, IPluginMethodMap } from './IAgent.js'
 import { IMessage } from './IMessage.js'
 
+/**
+ *  The allowed columns for querying different data types in the {@link IDataStoreORM} interface.
+ *  @internal
+ */
+export const ALLOWED_COLUMNS = {
+  message: ['from', 'to', 'id', 'createdAt', 'expiresAt', 'threadId', 'type', 'raw', 'replyTo', 'replyUrl'],
+  claim: [
+    'context',
+    'credentialType',
+    'type',
+    'value',
+    'isObj',
+    'id',
+    'issuer',
+    'subject',
+    'expirationDate',
+    'issuanceDate',
+  ],
+  credential: ['context', 'type', 'id', 'issuer', 'subject', 'expirationDate', 'issuanceDate', 'hash'],
+  presentation: ['context', 'type', 'id', 'holder', 'verifier', 'expirationDate', 'issuanceDate'],
+  identifier: ['did', 'alias', 'provider'],
+} as const
+
 /**
  * Represents the sort order of results from a {@link FindArgs} query.
  *
@@ -70,7 +93,7 @@ export interface FindArgs<TColumns> {
  * @deprecated This type will be removed in future versions of this plugin interface.
  * @beta This API may change without a BREAKING CHANGE notice.
  */
-export type TIdentifiersColumns = 'did' | 'alias' | 'provider'
+export type TIdentifiersColumns = (typeof ALLOWED_COLUMNS.identifier)[number]
 
 /**
  * The columns that can be queried for an {@link IMessage}
@@ -78,17 +101,7 @@ export type TIdentifiersColumns = 'did' | 'alias' | 'provider'
  * See {@link IDataStoreORM.dataStoreORMGetMessagesCount}
  * @beta This API may change without a BREAKING CHANGE notice.
  */
-export type TMessageColumns =
-  | 'from'
-  | 'to'
-  | 'id'
-  | 'createdAt'
-  | 'expiresAt'
-  | 'threadId'
-  | 'type'
-  | 'raw'
-  | 'replyTo'
-  | 'replyUrl'
+export type TMessageColumns = (typeof ALLOWED_COLUMNS.message)[number]
 
 /**
  * The columns that can be searched for a {@link VerifiableCredential}
@@ -98,15 +111,7 @@ export type TMessageColumns =
  *
  * @beta This API may change without a BREAKING CHANGE notice.
  */
-export type TCredentialColumns =
-  | 'context'
-  | 'type'
-  | 'id'
-  | 'issuer'
-  | 'subject'
-  | 'expirationDate'
-  | 'issuanceDate'
-  | 'hash'
+export type TCredentialColumns = (typeof ALLOWED_COLUMNS.credential)[number]
 
 /**
  * The columns that can be searched for the claims of a {@link VerifiableCredential}
@@ -116,17 +121,7 @@ export type TCredentialColumns =
  *
  * @beta This API may change without a BREAKING CHANGE notice.
  */
-export type TClaimsColumns =
-  | 'context'
-  | 'credentialType'
-  | 'type'
-  | 'value'
-  | 'isObj'
-  | 'id'
-  | 'issuer'
-  | 'subject'
-  | 'expirationDate'
-  | 'issuanceDate'
+export type TClaimsColumns = (typeof ALLOWED_COLUMNS.claim)[number]
 
 /**
  * The columns that can be searched for a {@link VerifiablePresentation}
@@ -136,14 +131,7 @@ export type TClaimsColumns =
  *
  * @beta This API may change without a BREAKING CHANGE notice.
  */
-export type TPresentationColumns =
-  | 'context'
-  | 'type'
-  | 'id'
-  | 'holder'
-  | 'verifier'
-  | 'expirationDate'
-  | 'issuanceDate'
+export type TPresentationColumns = (typeof ALLOWED_COLUMNS.presentation)[number]
 
 /**
  * This context can be used for Veramo Agents that are created behind an authorization mechanism, that attaches a DID
diff --git a/packages/data-store-json/src/data-store-json.ts b/packages/data-store-json/src/data-store-json.ts
index 770816e6d..d006c1861 100644
--- a/packages/data-store-json/src/data-store-json.ts
+++ b/packages/data-store-json/src/data-store-json.ts
@@ -1,4 +1,5 @@
 import {
+  ALLOWED_COLUMNS,
   AuthorizedDIDContext,
   FindArgs,
   IAgentPlugin,
@@ -587,6 +588,8 @@ function buildQuery<T extends Partial<Record<PossibleColumns, any>>>(
     })
   }
 
+  const allowedColumns = Object.values(ALLOWED_COLUMNS).flat()
+
   if (input.order && input.order.length > 0) {
     filteredCollection.sort((a: T, b: T) => {
       let result = 0
@@ -597,6 +600,9 @@ function buildQuery<T extends Partial<Record<PossibleColumns, any>>>(
         if (!col) {
           break
         }
+        if (!allowedColumns.includes(col)) {
+          throw new Error(`Invalid column name: ${col}`)
+        }
         const colA = a[col]
         const colB = b[col]
         if (typeof colA?.getTime === 'function') {
diff --git a/packages/data-store/src/data-store-orm.ts b/packages/data-store/src/data-store-orm.ts
index 0a70351fa..359844b89 100644
--- a/packages/data-store/src/data-store-orm.ts
+++ b/packages/data-store/src/data-store-orm.ts
@@ -1,4 +1,5 @@
 import {
+  ALLOWED_COLUMNS,
   AuthorizedDIDContext,
   FindArgs,
   IAgentPlugin,
@@ -422,7 +423,11 @@ function decorateQB(
   if (input?.take) qb = qb.limit(input.take)
 
   if (input?.order) {
+    const allowedColumns = getAllowedColumnsForTable(tableName)
     for (const item of input.order) {
+      if (!allowedColumns.includes(item.column)) {
+        throw new Error(`Invalid column name: ${item.column}`)
+      }
       qb = qb.addSelect(
         qb.connection.driver.escape(tableName) + '.' + qb.connection.driver.escape(item.column),
         item.column,
@@ -432,3 +437,7 @@ function decorateQB(
   }
   return qb
 }
+
+function getAllowedColumnsForTable(tableName: string): readonly string[] {
+  return ALLOWED_COLUMNS[tableName as keyof typeof ALLOWED_COLUMNS] || []
+}