Skip to content

prevent SQL injection by using parameterized queries.#709

Open
hitakshiA wants to merge 1 commit intotekdi:mainfrom
hitakshiA:fix/sql-injection-parameterized-queries
Open

prevent SQL injection by using parameterized queries.#709
hitakshiA wants to merge 1 commit intotekdi:mainfrom
hitakshiA:fix/sql-injection-parameterized-queries

Conversation

@hitakshiA
Copy link
Copy Markdown

@hitakshiA hitakshiA commented Mar 31, 2026

Replaced raw string interpolation in SQL queries with parameterized queries ($1, $2) across 3 files to prevent SQL injection (OWASP A03:2021).

What changed

  • src/fields/fields.service.ts — 11 queries parameterized (SELECT, INSERT, UPDATE, DELETE)
  • src/tenant/tenant.service.ts — 1 query parameterized
  • src/user/user.service.ts — 1 query parameterized

Before

typescript
const query = SELECT * FROM public."Roles" WHERE "tenantId" = '${tenantData.tenantId}';
await this.tenantRepository.query(query);

After

const query = SELECT * FROM public."Roles" WHERE "tenantId" = $1;
await this.tenantRepository.query(query, [tenantData.tenantId]

Summary by CodeRabbit

Chores

  • Strengthened database query implementations in field management, tenant, and user services to improve system stability and data consistency.
  • Enhanced parameter binding and data handling mechanisms to ensure more reliable database operations and reduce potential inconsistencies.
  • Refined query execution processes across services to improve overall system robustness.

…ing interpolation in SQL queries with parameterized

queries (, , etc.) across fields.service.ts, tenant.service.ts,
and user.service.ts to prevent SQL injection attacks (OWASP A03:2021).

13 vulnerable query instances fixed.
@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Mar 31, 2026

Walkthrough

This pull request converts SQL queries across three service files from vulnerable string interpolation to parameterized SQL queries using positional placeholders, preventing SQL injection attacks. The getFieldData method signature is updated to accept and forward a params array.

Changes

Cohort / File(s) Summary
SQL parameterization in fields service
src/fields/fields.service.ts
Refactored multiple SQL query paths to use positional parameter placeholders ($1, $2, etc.) instead of string interpolation for context, contextType, option values, field IDs, tenant IDs, and search terms. Updated getFieldData signature to accept params array and forward it to repository queries. Modified field option existence checks and JSONB operations to use typed parameter binding.
SQL parameterization in tenant and user services
src/tenant/tenant.service.ts, src/user/user.service.ts
Converted inline SQL queries to use parameterized statements: replaced tenantId interpolation in role-fetch query and cohortIds/academicYearId interpolation in cohort lookup query with bound parameter arrays.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~22 minutes

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title directly and accurately summarizes the primary objective of the changeset: preventing SQL injection through parameterized queries. It is concise, specific, and clearly conveys the main change across all modified files.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@sonarqubecloud
Copy link
Copy Markdown

@hitakshiA hitakshiA changed the title prevent SQL injection by using parameterized queries.Replace raw str… prevent SQL injection by using parameterized queries. Mar 31, 2026
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/fields/fields.service.ts`:
- Around line 399-405: The SQL concatenates request-derived identifiers (e.g.
sourceDetails.table, tableName, whereClause, order) into raw queries used by
fieldsValuesRepository.query, leaving SQL injection risk; fix by validating and
whitelisting table and column identifiers (e.g. enforce sourceDetails.table
against an allow-list or enum and map where/order options to predefined clauses)
or switch to a safe query builder (e.g. knex/TypeORM QueryBuilder) instead of
string concatenation, then pass only literal values via $n parameters (e.g. the
sourceFieldName["value"]); apply the same validation/whitelisting approach to
the other occurrences you flagged (lines ~538-545, 700-724, 738-750, 1402-1403,
1501-1503) and replace any raw fragment interpolation with mapped safe fragments
or builder methods before calling fieldsValuesRepository.query.
- Around line 68-79: The whereClause currently appends an independent OR clause
for contextType which allows matching that contextType across any context;
instead, when both requiredData.context and requiredData.contextType are
provided, combine them into a single predicate so contextType is scoped to that
context (e.g. append OR (context = $i AND "contextType" = $j) or build
conditions via an array and join with proper parentheses). Update the logic
around whereClause/params in the same block (references: whereClause, params,
requiredData.context, requiredData.contextType, getFieldData) so that the
contextType clause is never emitted standalone if a context value exists. Ensure
parameter indices remain correct when pushing combined conditions before calling
this.getFieldData.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 3a86a111-4696-4e2b-85c0-936888ee0cb9

📥 Commits

Reviewing files that changed from the base of the PR and between 978b447 and ee03b05.

📒 Files selected for processing (3)
  • src/fields/fields.service.ts
  • src/tenant/tenant.service.ts
  • src/user/user.service.ts

Comment on lines +68 to +79
const params = [];
if (requiredData.context) {
whereClause += ` OR context = '${requiredData.context}' AND "contextType" IS NULL`;
params.push(requiredData.context);
whereClause += ` OR context = $${params.length} AND "contextType" IS NULL`;
}

if (requiredData.contextType) {
whereClause += ` OR "contextType" = '${requiredData.contextType}'`;
params.push(requiredData.contextType);
whereClause += ` OR "contextType" = $${params.length}`;
}

const data = await this.getFieldData(whereClause);
const data = await this.getFieldData(whereClause, params);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Keep contextType scoped to the requested context.

When both filters are present, Line 76 turns the predicate into a standalone OR "contextType" = $n, so this query can pull fields from other contexts that happen to share the same contextType. That broadens getFormCustomField() beyond the requested schema.

🛠️ Suggested fix
-      const params = [];
-      if (requiredData.context) {
-        params.push(requiredData.context);
-        whereClause += ` OR context = $${params.length} AND "contextType" IS NULL`;
-      }
-
-      if (requiredData.contextType) {
-        params.push(requiredData.contextType);
-        whereClause += ` OR "contextType" = $${params.length}`;
-      }
-
-      const data = await this.getFieldData(whereClause, params);
+      const params = [];
+      const clauses = ['(context IS NULL AND "contextType" IS NULL)'];
+
+      if (requiredData.context) {
+        params.push(requiredData.context);
+        clauses.push(`(context = $${params.length} AND "contextType" IS NULL)`);
+      }
+
+      if (requiredData.contextType) {
+        if (requiredData.context) {
+          params.push(requiredData.context, requiredData.contextType);
+          clauses.push(`(context = $${params.length - 1} AND "contextType" = $${params.length})`);
+        } else {
+          params.push(requiredData.contextType);
+          clauses.push(`("contextType" = $${params.length})`);
+        }
+      }
+
+      const data = await this.getFieldData(clauses.join(" OR "), params);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const params = [];
if (requiredData.context) {
whereClause += ` OR context = '${requiredData.context}' AND "contextType" IS NULL`;
params.push(requiredData.context);
whereClause += ` OR context = $${params.length} AND "contextType" IS NULL`;
}
if (requiredData.contextType) {
whereClause += ` OR "contextType" = '${requiredData.contextType}'`;
params.push(requiredData.contextType);
whereClause += ` OR "contextType" = $${params.length}`;
}
const data = await this.getFieldData(whereClause);
const data = await this.getFieldData(whereClause, params);
const params = [];
const clauses = ['(context IS NULL AND "contextType" IS NULL)'];
if (requiredData.context) {
params.push(requiredData.context);
clauses.push(`(context = $${params.length} AND "contextType" IS NULL)`);
}
if (requiredData.contextType) {
if (requiredData.context) {
params.push(requiredData.context, requiredData.contextType);
clauses.push(`(context = $${params.length - 1} AND "contextType" = $${params.length})`);
} else {
params.push(requiredData.contextType);
clauses.push(`("contextType" = $${params.length})`);
}
}
const data = await this.getFieldData(clauses.join(" OR "), params);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/fields/fields.service.ts` around lines 68 - 79, The whereClause currently
appends an independent OR clause for contextType which allows matching that
contextType across any context; instead, when both requiredData.context and
requiredData.contextType are provided, combine them into a single predicate so
contextType is scoped to that context (e.g. append OR (context = $i AND
"contextType" = $j) or build conditions via an array and join with proper
parentheses). Update the logic around whereClause/params in the same block
(references: whereClause, params, requiredData.context,
requiredData.contextType, getFieldData) so that the contextType clause is never
emitted standalone if a context value exists. Ensure parameter indices remain
correct when pushing combined conditions before calling this.getFieldData.

Comment on lines +399 to +405
const query = `SELECT "name", "value"
FROM public.${fieldsData.sourceDetails.table}
WHERE value = $1
GROUP BY "name", "value"`;

const checkSourceData = await this.fieldsValuesRepository.query(
query
query, [sourceFieldName["value"]]
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

These queries are still injectable through raw identifiers/fragments.

$n placeholders only protect literal values. The touched lines still splice sourceDetails.table / tableName / whereClause / order directly into the SQL text, and in this service those values are request-derived or persisted from earlier requests. The injection surface is still open even after parameterizing the value operands.

🔒 Hardening pattern
+ private readonly ALLOWED_SOURCE_TABLES = new Set([
+   "state",
+   "district",
+   "block",
+   "village",
+   // add every supported source table here
+ ]);
+
+ private getSafeSourceTable(tableName: string): string {
+   if (!this.ALLOWED_SOURCE_TABLES.has(tableName)) {
+     throw new Error(`Unsupported source table: ${tableName}`);
+   }
+   return `"${tableName}"`;
+ }

Use that helper anywhere a table name is currently interpolated, and build WHERE / ORDER BY from validated enums instead of accepting raw SQL fragments.

Also applies to: 538-545, 700-724, 738-750, 1402-1403, 1501-1503

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/fields/fields.service.ts` around lines 399 - 405, The SQL concatenates
request-derived identifiers (e.g. sourceDetails.table, tableName, whereClause,
order) into raw queries used by fieldsValuesRepository.query, leaving SQL
injection risk; fix by validating and whitelisting table and column identifiers
(e.g. enforce sourceDetails.table against an allow-list or enum and map
where/order options to predefined clauses) or switch to a safe query builder
(e.g. knex/TypeORM QueryBuilder) instead of string concatenation, then pass only
literal values via $n parameters (e.g. the sourceFieldName["value"]); apply the
same validation/whitelisting approach to the other occurrences you flagged
(lines ~538-545, 700-724, 738-750, 1402-1403, 1501-1503) and replace any raw
fragment interpolation with mapped safe fragments or builder methods before
calling fieldsValuesRepository.query.

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