Skip to content

Dev#178

Merged
vernu merged 31 commits intomainfrom
dev
Feb 6, 2026
Merged

Dev#178
vernu merged 31 commits intomainfrom
dev

Conversation

@vernu
Copy link
Owner

@vernu vernu commented Feb 6, 2026

No description provided.

@vercel
Copy link

vercel bot commented Feb 6, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
textbee Ready Ready Preview, Comment Feb 6, 2026 4:10pm
textbee (staging) Ready Ready Preview, Comment Feb 6, 2026 4:10pm

Request Review

return await this.deviceModel.findByIdAndUpdate(
deviceId,
{ $set: input },
{ $set: updateData },

Check failure

Code scanning / CodeQL

Database query built from user-controlled sources High

This query object depends on a
user-provided value
.
This query object depends on a
user-provided value
.

Copilot Autofix

AI about 1 month ago

In general, to fix this kind of issue, you must ensure that user-controlled data is treated strictly as values and not as query/update operators. For MongoDB/Mongoose updates, that means never passing a raw user object directly to operators like $set, but instead constructing a new object that only contains whitelisted, expected fields and simple literal values. This both prevents operator injection and guards against mass‑assignment of unintended properties.

For this specific case, the fix is to build updateData explicitly from allowed fields of RegisterDeviceInputDTO instead of { ...input }. Additionally, for any nested object like simInfo, we should build that subdocument field-by-field and/or treat the entire simInfo as a literal by placing it under a non-operator key. We should also ensure we do not allow users to override internal fields (e.g., user, _id) or to add arbitrary top-level keys.

Concretely, in api/src/gateway/gateway.service.ts:

  • In registerDevice, instead of const deviceData: any = { ...input, user }, construct deviceData by whitelisting known properties of RegisterDeviceInputDTO (e.g., model, brand, buildId, name, appVersionCode, simInfo, enabled, etc.). This ensures that deviceData cannot contain unexpected MongoDB operators on properties that will later be reused in updates.
  • In updateDevice, replace const updateData: any = { ...input } with a new object that only includes explicitly allowed properties (same whitelist as above) and builds simInfo as a plain object. Because findByIdAndUpdate is using { $set: updateData }, we must ensure updateData itself cannot contain any $-prefixed keys.
  • Preserve existing logic for setting default name, normalizing enabled, and enriching simInfo.lastUpdated.

This change is localized to gateway.service.ts and does not change API behavior from a legitimate client’s perspective, but it removes the reliance on a tainted, unstructured object.

Suggested changeset 1
api/src/gateway/gateway.service.ts

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/api/src/gateway/gateway.service.ts b/api/src/gateway/gateway.service.ts
--- a/api/src/gateway/gateway.service.ts
+++ b/api/src/gateway/gateway.service.ts
@@ -46,7 +46,16 @@
       buildId: input.buildId,
     })
 
-    const deviceData: any = { ...input, user }
+    // Build device data explicitly from allowed fields to avoid propagating arbitrary keys
+    const deviceData: any = {
+      user,
+      model: input.model,
+      brand: input.brand,
+      buildId: input.buildId,
+      name: input.name,
+      appVersionCode: input.appVersionCode,
+      enabled: input.enabled,
+    }
     
     // Set default name to "brand model" if not provided
     if (!deviceData.name && input.brand && input.model) {
@@ -98,7 +107,15 @@
       input.enabled = true;
     }
 
-    const updateData: any = { ...input }
+    // Build update data from a whitelist of allowed fields to prevent NoSQL injection
+    const updateData: any = {
+      model: input.model,
+      brand: input.brand,
+      buildId: input.buildId,
+      name: input.name,
+      appVersionCode: input.appVersionCode,
+      enabled: input.enabled,
+    }
     
     // Handle simInfo if provided
     if (input.simInfo) {
EOF
@@ -46,7 +46,16 @@
buildId: input.buildId,
})

const deviceData: any = { ...input, user }
// Build device data explicitly from allowed fields to avoid propagating arbitrary keys
const deviceData: any = {
user,
model: input.model,
brand: input.brand,
buildId: input.buildId,
name: input.name,
appVersionCode: input.appVersionCode,
enabled: input.enabled,
}

// Set default name to "brand model" if not provided
if (!deviceData.name && input.brand && input.model) {
@@ -98,7 +107,15 @@
input.enabled = true;
}

const updateData: any = { ...input }
// Build update data from a whitelist of allowed fields to prevent NoSQL injection
const updateData: any = {
model: input.model,
brand: input.brand,
buildId: input.buildId,
name: input.name,
appVersionCode: input.appVersionCode,
enabled: input.enabled,
}

// Handle simInfo if provided
if (input.simInfo) {
Copilot is powered by AI and may make mistakes. Always verify output.
Unable to commit as this autofix suggestion is now outdated
Comment on lines +682 to +691
const existingSMS = await this.smsModel.findOne({
device: device._id,
type: SMSType.RECEIVED,
sender: dto.sender,
message: dto.message,
receivedAt: {
$gte: toleranceStart,
$lte: toleranceEnd,
},
})

Check failure

Code scanning / CodeQL

Database query built from user-controlled sources High

This query object depends on a
user-provided value
.

Copilot Autofix

AI about 1 month ago

In general, to fix this kind of NoSQL injection risk, you must ensure that any user-controlled data used in a MongoDB query is treated strictly as a literal value and cannot be interpreted as a query object or operator. This is commonly done by (a) validating that the input is of an expected primitive type (string/number/boolean) before using it in the query, or (b) explicitly wrapping the value in an operator like $eq, which forces MongoDB to interpret the data as a scalar comparison value.

For this specific code, the best minimal fix without changing existing functionality is to (1) add runtime checks that dto.sender and dto.message are strings and that receivedAt is a valid Date, and (2) wrap the user-controlled fields in $eq in the findOne query. This keeps the semantics (find an SMS with exact same device, type, sender, message, and a receivedAt within a tolerance window) but prevents any possibility of passing an object that Mongo could interpret as query operators.

Concretely in api/src/gateway/gateway.service.ts:

  • In receiveSMS, augment the existing validation block to also verify typeof dto.sender === 'string' and typeof dto.message === 'string'. Also check that receivedAt resolves to a valid Date (i.e., not Invalid Date) before using it.
  • Update the this.smsModel.findOne({ ... }) call so that sender and message are expressed as { $eq: dto.sender } and { $eq: dto.message }. device and type are either ObjectId/enum from server-side code, so they do not need this treatment.

No new methods or external libraries are required; only in-place validation and query changes in receiveSMS are needed. All changes occur within the shown snippet of GatewayService.receiveSMS.


Suggested changeset 1
api/src/gateway/gateway.service.ts

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/api/src/gateway/gateway.service.ts b/api/src/gateway/gateway.service.ts
--- a/api/src/gateway/gateway.service.ts
+++ b/api/src/gateway/gateway.service.ts
@@ -657,7 +657,9 @@
     if (
       (!dto.receivedAt && !dto.receivedAtInMillis) ||
       !dto.sender ||
-      !dto.message
+      !dto.message ||
+      typeof dto.sender !== 'string' ||
+      typeof dto.message !== 'string'
     ) {
       console.log('Invalid received SMS data')
       throw new HttpException(
@@ -679,6 +681,17 @@
       ? new Date(dto.receivedAtInMillis)
       : dto.receivedAt
 
+    if (!(receivedAt instanceof Date) || isNaN(receivedAt.getTime())) {
+      console.log('Invalid receivedAt value')
+      throw new HttpException(
+        {
+          success: false,
+          error: 'Invalid received SMS data',
+        },
+        HttpStatus.BAD_REQUEST,
+      )
+    }
+
     // Deduplication: Check for existing SMS with same device, sender, message, and receivedAt (within ±5 seconds tolerance)
     const toleranceMs = 5000 // 5 seconds
     const toleranceStart = new Date(receivedAt.getTime() - toleranceMs)
@@ -687,8 +700,8 @@
     const existingSMS = await this.smsModel.findOne({
       device: device._id,
       type: SMSType.RECEIVED,
-      sender: dto.sender,
-      message: dto.message,
+      sender: { $eq: dto.sender },
+      message: { $eq: dto.message },
       receivedAt: {
         $gte: toleranceStart,
         $lte: toleranceEnd,
EOF
@@ -657,7 +657,9 @@
if (
(!dto.receivedAt && !dto.receivedAtInMillis) ||
!dto.sender ||
!dto.message
!dto.message ||
typeof dto.sender !== 'string' ||
typeof dto.message !== 'string'
) {
console.log('Invalid received SMS data')
throw new HttpException(
@@ -679,6 +681,17 @@
? new Date(dto.receivedAtInMillis)
: dto.receivedAt

if (!(receivedAt instanceof Date) || isNaN(receivedAt.getTime())) {
console.log('Invalid receivedAt value')
throw new HttpException(
{
success: false,
error: 'Invalid received SMS data',
},
HttpStatus.BAD_REQUEST,
)
}

// Deduplication: Check for existing SMS with same device, sender, message, and receivedAt (within ±5 seconds tolerance)
const toleranceMs = 5000 // 5 seconds
const toleranceStart = new Date(receivedAt.getTime() - toleranceMs)
@@ -687,8 +700,8 @@
const existingSMS = await this.smsModel.findOne({
device: device._id,
type: SMSType.RECEIVED,
sender: dto.sender,
message: dto.message,
sender: { $eq: dto.sender },
message: { $eq: dto.message },
receivedAt: {
$gte: toleranceStart,
$lte: toleranceEnd,
Copilot is powered by AI and may make mistakes. Always verify output.
Unable to commit as this autofix suggestion is now outdated
@vernu vernu merged commit 709f05f into main Feb 6, 2026
8 of 9 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant