Skip to content

fix: prevent SQL injection in model-data API#304

Merged
chilingling merged 5 commits intoopentiny:developfrom
hexqi:fix/sql-inject
Apr 25, 2026
Merged

fix: prevent SQL injection in model-data API#304
chilingling merged 5 commits intoopentiny:developfrom
hexqi:fix/sql-inject

Conversation

@hexqi
Copy link
Copy Markdown
Collaborator

@hexqi hexqi commented Apr 25, 2026

Summary

Security impact

模型数据接口 queryApi 接受用户传入的 fields(查询字段)、orderBy(排序字段)、orderType(排序方式)参数(前端功能暂未开放入口,但可以被直接调用接口攻击),这些参数通过字符串拼接直接写入 SQL 语句,未做任何校验或过滤,攻击者可注入任意 SQL 表达式,足以造成敏感数据泄露与数据库结构暴露。
Blocks all SQL injection vectors reported in the security audit:

  • fields subquery injection (@@version, information_schema, SELECT password FROM t_user)
  • orderBy/orderType expression injection (SLEEP(), IF(), ; DROP TABLE)
  • update condition column injection

解决方法:
添加必要的校验,参数字段需在模型表字段中 & 正则校验输入非法字符

  • Add SqlIdentifierValidator utility for centralized SQL identifier format validation (regex ^[a-zA-Z_][a-zA-Z0-9_]*$) and order type enum check (ASC/DESC)
  • Add model metadata whitelist validation in DynamicService and DynamicModelServicefields and orderBy are restricted to columns defined in the model's parameters
  • Add @Pattern validation annotations on DynamicQuery DTO (nameEn, fields, orderBy, orderType)
  • Fix DynamicService.update() — previously only validated data keys, now also validates params (WHERE condition) keys
  • Add identifier validation to DynamicModelService.dynamicQuery(), createData(), updateDateById()
  • Resolve circular dependency between DynamicModelService and ModelServiceImpl via @Lazy

Test plan

  • 359 unit tests pass (28 new security tests + 331 existing, 1 skipped)
  • Interface-level verification: all 10 attack payloads blocked, normal queries unaffected
  • Clean build + jar startup verified

Summary by CodeRabbit

Release Notes

  • New Features

    • Dynamic query operations now enforce validation on field and column references to ensure consistent naming conventions.
    • Order type parameters are now restricted to ASC or DESC values only.
  • Tests

    • Added comprehensive test coverage for dynamic query input validation to verify proper handling of various input formats.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 25, 2026

Warning

Rate limit exceeded

@hexqi has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 10 minutes and 48 seconds before requesting another review.

Your organization is not enrolled in usage-based pricing. Contact your admin to enable usage-based pricing to continue reviews beyond the rate limit, or try again in 10 minutes and 48 seconds.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 6ff492b2-0519-444d-bea7-416beaeeb0ab

📥 Commits

Reviewing files that changed from the base of the PR and between 712af3a and afdb1e5.

📒 Files selected for processing (4)
  • base/src/main/java/com/tinyengine/it/dynamic/dto/DynamicQuery.java
  • base/src/main/java/com/tinyengine/it/dynamic/service/DynamicModelService.java
  • base/src/main/java/com/tinyengine/it/dynamic/service/DynamicService.java
  • base/src/test/java/com/tinyengine/it/dynamic/service/DynamicServiceSqlInjectionTest.java

Walkthrough

This PR introduces SQL injection prevention by adding a new SqlIdentifierValidator utility class and integrating input validation across dynamic query and model services. Validation is applied to table/column identifiers, field lists, ordering parameters, and condition keys using regex patterns and allowlist approaches derived from model metadata.

Changes

Cohort / File(s) Summary
SQL Validation Utility
base/src/main/java/com/tinyengine/it/common/utils/SqlIdentifierValidator.java
New utility class with static methods for validating SQL identifiers, lists of identifiers, and order types against strict regex patterns. Throws IllegalArgumentException for invalid inputs.
DTO Input Validation
base/src/main/java/com/tinyengine/it/dynamic/dto/DynamicQuery.java
Added @Pattern annotations to nameEn, orderBy, and orderType fields, plus list element validation in fields to enforce identifier format and order type constraints.
Service Validation Integration
base/src/main/java/com/tinyengine/it/dynamic/service/DynamicModelService.java, base/src/main/java/com/tinyengine/it/dynamic/service/DynamicService.java
Integrated SqlIdentifierValidator calls to sanitize query parameters, enforce field allowlisting via model metadata, and validate column names in insert/update operations. Replaced Lombok @RequiredArgsConstructor with explicit constructor in DynamicModelService.
Test Coverage
base/src/test/java/com/tinyengine/it/common/utils/SqlIdentifierValidatorTest.java, base/src/test/java/com/tinyengine/it/dynamic/service/DynamicModelServiceTest.java, base/src/test/java/com/tinyengine/it/dynamic/service/DynamicServiceSqlInjectionTest.java
Added unit tests for validator behavior including SQLi payload rejection. Updated existing model service tests to mock ModelService dependency and adjust query parameters. Added comprehensive integration tests for SQL injection prevention across query and update operations.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Poem

🐰 With whiskers twitching and nose held high,
I validate each query that passes by,
No sneaky SQL shall slip on through—
The regex guards will see them through!
Safe from injection, clean and bright,
Our data dances in the night! ✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 13.79% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely summarizes the main security fix: preventing SQL injection attacks in the model-data API, which is the primary objective of this changeset.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ 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.

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: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
base/src/main/java/com/tinyengine/it/dynamic/service/DynamicModelService.java (1)

296-322: ⚠️ Potential issue | 🟠 Major

orderType is validated but never applied to the generated SQL — DESC requests are silently ignored.

queryWithPage only forwards orderBy into dynamicQuery; dto.getOrderType() is validated in validateQueryFields but never used when building ORDER BY. Combined with the test change (from orderBy="id DESC" to orderBy="id" + orderType="DESC"), DESC ordering is lost at runtime even though the API contract now advertises a separate orderType.

🐛 Proposed fix: propagate orderType and append it to the SQL
 	public Map<String, Object> queryWithPage(DynamicQuery dto) {
 		String tableName = getTableName(dto.getNameEn());
 		List<String> fields = dto.getFields();
 		Map<String, Object> conditions = dto.getParams();
 		String orderBy = dto.getOrderBy();
+		String orderType = dto.getOrderType();
 		Integer pageNum = dto.getCurrentPage();
 		Integer pageSize = dto.getPageSize();

 		validateQueryFields(dto);
@@
-		List<Map<String, Object>> data = dynamicQuery(
-			tableName, fields, conditions, orderBy, limit);
+		String effectiveOrderBy = (orderBy != null && !orderBy.isEmpty() && orderType != null && !orderType.isEmpty())
+				? orderBy + " " + orderType
+				: orderBy;
+		List<Map<String, Object>> data = dynamicQuery(
+			tableName, fields, conditions, effectiveOrderBy, limit);

Alternatively, extend dynamicQuery's signature to take orderType explicitly and append it after identifier validation.

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

In
`@base/src/main/java/com/tinyengine/it/dynamic/service/DynamicModelService.java`
around lines 296 - 322, The ordering direction from DynamicQuery
(dto.getOrderType()) is validated in validateQueryFields but never applied in
queryWithPage, causing DESC requests to be ignored; update queryWithPage to
combine the validated orderBy and orderType (or extend dynamicQuery to accept an
orderType param) and pass the composed ordering into dynamicQuery (e.g., build
"orderBy + ' ' + orderType" after validation) so that dynamicQuery receives the
direction and ORDER BY is correctly emitted; refer to queryWithPage,
validateQueryFields, dynamicQuery and dto.getOrderType when making the change.
🧹 Nitpick comments (3)
base/src/main/java/com/tinyengine/it/dynamic/service/DynamicService.java (2)

224-240: Use SqlIdentifierValidator.validate for the table name too, instead of a parallel regex.

Line 229 hand-rolls "^[a-zA-Z_][a-zA-Z0-9_]*$" — the exact pattern already encapsulated by SqlIdentifierValidator.IDENTIFIER_PATTERN. Using the validator keeps all identifier rules in one place so future changes (e.g. allowing dotted names, reserving keywords) need only one edit.

♻️ Proposed refactor
     private void validateTableAndData(String tableName, Map<String, Object> data) {
         if (tableName == null || tableName.trim().isEmpty()) {
             throw new IllegalArgumentException("表名不能为空");
         }
-
-        if (!tableName.matches("^[a-zA-Z_][a-zA-Z0-9_]*$")) {
-            throw new IllegalArgumentException("表名格式不正确");
-        }
-
+        SqlIdentifierValidator.validate(tableName);
         if (data == null || data.isEmpty()) {
             throw new IllegalArgumentException("数据不能为空");
         }
-
         for (String field : data.keySet()) {
             SqlIdentifierValidator.validate(field);
         }
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@base/src/main/java/com/tinyengine/it/dynamic/service/DynamicService.java`
around lines 224 - 240, The table-name regex check in validateTableAndData
duplicates identifier rules; replace the manual pattern check with a call to
SqlIdentifierValidator.validate for the tableName so all identifier rules are
centralized. Keep the existing null/empty check (tableName == null ||
tableName.trim().isEmpty()) and then call
SqlIdentifierValidator.validate(tableName.trim()) (or validate(tableName) after
trimming) before proceeding; leave the existing data null/empty check and the
loop that validates fields unchanged.

27-29: Duplicate of DynamicModelService — extract the allowlist helper.

SYSTEM_FIELDS, getAllowedFields, extractProp, and validateQueryFields here are identical to the copies in DynamicModelService. A shared utility (e.g. ModelFieldAllowlist taking ModelService as a dependency) would eliminate the drift risk without changing behavior.

Also applies to: 170-222

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

In `@base/src/main/java/com/tinyengine/it/dynamic/service/DynamicService.java`
around lines 27 - 29, The duplicate allowlist logic in DynamicService
(SYSTEM_FIELDS, getAllowedFields, extractProp, validateQueryFields) is identical
to DynamicModelService — extract this into a shared helper class (e.g.,
ModelFieldAllowlist) that accepts the existing ModelService dependency and
exposes the allowlist operations; replace the duplicated methods in both
DynamicService and DynamicModelService with calls to
ModelFieldAllowlist.INSTANCE or an injected ModelFieldAllowlist instance, move
SYSTEM_FIELDS into that helper, and update usages in DynamicService (methods
named getAllowedFields, extractProp, validateQueryFields) to delegate to the new
helper to eliminate duplication and prevent drift.
base/src/main/java/com/tinyengine/it/dynamic/service/DynamicModelService.java (1)

38-40: SYSTEM_FIELDS, getAllowedFields, extractProp, and validateQueryFields are verbatim duplicates of DynamicService.

Both services now carry identical allowlisting logic. Any future change (e.g. adding a system column, tweaking the allowlist rule) must be applied in two places or the two endpoints will drift. Consider extracting to a shared helper (e.g. ModelFieldAllowlist) under com.tinyengine.it.common.utils and injecting it into both services.

Also applies to: 323-376

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

In
`@base/src/main/java/com/tinyengine/it/dynamic/service/DynamicModelService.java`
around lines 38 - 40, DynamicModelService duplicates allowlisting logic
(SYSTEM_FIELDS, getAllowedFields, extractProp, validateQueryFields) already
present in DynamicService; extract this logic into a single shared helper class
(e.g., com.tinyengine.it.common.utils.ModelFieldAllowlist) that encapsulates the
SYSTEM_FIELDS set and methods for getAllowedFields, extractProp, and
validateQueryFields, then replace the local implementations in both
DynamicModelService and DynamicService with dependency-injected usages of
ModelFieldAllowlist (constructor or field injection) so both services delegate
to the shared helper for future-proof, single-source maintenance.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@base/src/main/java/com/tinyengine/it/dynamic/dto/DynamicQuery.java`:
- Around line 25-26: The DTO's orderType field (`@Pattern` on orderType) is
case-sensitive while SqlIdentifierValidator.validateOrderType accepts
case-insensitive values, causing valid lowercase values to be rejected; to fix,
make the DTO accept case-insensitive values (e.g., add the (?i) inline flag to
the `@Pattern` on the orderType field) so it aligns with
SqlIdentifierValidator.validateOrderType and the tests
(validateOrderTypeAsc/validateOrderTypeDesc) and does not reject "asc"/"desc" at
the `@Valid` DTO layer.

In
`@base/src/main/java/com/tinyengine/it/dynamic/service/DynamicModelService.java`:
- Around line 208-210: The code in DynamicModelService using
orderBy.replaceAll("\\s+(ASC|DESC)$", "") is case-sensitive and fails to strip
"asc"/"desc" in lowercase, causing SqlIdentifierValidator.validate to receive
the trailing order token; change the strip to be case-insensitive (e.g. use a
case-insensitive regex like "(?i)\\s+(ASC|DESC)$" or normalize case before
stripping) so that orderBy is cleaned correctly before calling
SqlIdentifierValidator.validate; ensure this change touches the orderBy handling
in the dynamicQuery path that currently uses orderBy.replaceAll and still allows
SqlIdentifierValidator.validateOrderType to accept "asc"/"desc".

In `@base/src/main/java/com/tinyengine/it/dynamic/service/DynamicService.java`:
- Around line 65-67: The call to validateTableAndData(dto.getParams()) makes
queryWithPage reject empty or null params; change the validation so
queryWithPage only validates condition keys when params are present instead of
enforcing "non-empty data". Create a new helper (e.g.,
validateConditionKeys(List<...> params)) that iterates params and validates each
condition key/format but returns silently for null/empty lists, and replace the
validateTableAndData(dto.getNameEn(), dto.getParams()) call in queryWithPage
with validateConditionKeys(dto.getParams()); keep the original
validateTableAndData or a separate non-empty-enforcing helper for operations
that must not accept empty payloads (like update if required). Add a regression
test for queryWithPage that asserts success with params == null and with params
== empty list.

---

Outside diff comments:
In
`@base/src/main/java/com/tinyengine/it/dynamic/service/DynamicModelService.java`:
- Around line 296-322: The ordering direction from DynamicQuery
(dto.getOrderType()) is validated in validateQueryFields but never applied in
queryWithPage, causing DESC requests to be ignored; update queryWithPage to
combine the validated orderBy and orderType (or extend dynamicQuery to accept an
orderType param) and pass the composed ordering into dynamicQuery (e.g., build
"orderBy + ' ' + orderType" after validation) so that dynamicQuery receives the
direction and ORDER BY is correctly emitted; refer to queryWithPage,
validateQueryFields, dynamicQuery and dto.getOrderType when making the change.

---

Nitpick comments:
In
`@base/src/main/java/com/tinyengine/it/dynamic/service/DynamicModelService.java`:
- Around line 38-40: DynamicModelService duplicates allowlisting logic
(SYSTEM_FIELDS, getAllowedFields, extractProp, validateQueryFields) already
present in DynamicService; extract this logic into a single shared helper class
(e.g., com.tinyengine.it.common.utils.ModelFieldAllowlist) that encapsulates the
SYSTEM_FIELDS set and methods for getAllowedFields, extractProp, and
validateQueryFields, then replace the local implementations in both
DynamicModelService and DynamicService with dependency-injected usages of
ModelFieldAllowlist (constructor or field injection) so both services delegate
to the shared helper for future-proof, single-source maintenance.

In `@base/src/main/java/com/tinyengine/it/dynamic/service/DynamicService.java`:
- Around line 224-240: The table-name regex check in validateTableAndData
duplicates identifier rules; replace the manual pattern check with a call to
SqlIdentifierValidator.validate for the tableName so all identifier rules are
centralized. Keep the existing null/empty check (tableName == null ||
tableName.trim().isEmpty()) and then call
SqlIdentifierValidator.validate(tableName.trim()) (or validate(tableName) after
trimming) before proceeding; leave the existing data null/empty check and the
loop that validates fields unchanged.
- Around line 27-29: The duplicate allowlist logic in DynamicService
(SYSTEM_FIELDS, getAllowedFields, extractProp, validateQueryFields) is identical
to DynamicModelService — extract this into a shared helper class (e.g.,
ModelFieldAllowlist) that accepts the existing ModelService dependency and
exposes the allowlist operations; replace the duplicated methods in both
DynamicService and DynamicModelService with calls to
ModelFieldAllowlist.INSTANCE or an injected ModelFieldAllowlist instance, move
SYSTEM_FIELDS into that helper, and update usages in DynamicService (methods
named getAllowedFields, extractProp, validateQueryFields) to delegate to the new
helper to eliminate duplication and prevent drift.
🪄 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: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 7d48adf4-048b-4233-9ad4-80ee21fc5b4c

📥 Commits

Reviewing files that changed from the base of the PR and between c5147b9 and 712af3a.

📒 Files selected for processing (7)
  • base/src/main/java/com/tinyengine/it/common/utils/SqlIdentifierValidator.java
  • base/src/main/java/com/tinyengine/it/dynamic/dto/DynamicQuery.java
  • base/src/main/java/com/tinyengine/it/dynamic/service/DynamicModelService.java
  • base/src/main/java/com/tinyengine/it/dynamic/service/DynamicService.java
  • base/src/test/java/com/tinyengine/it/common/utils/SqlIdentifierValidatorTest.java
  • base/src/test/java/com/tinyengine/it/dynamic/service/DynamicModelServiceTest.java
  • base/src/test/java/com/tinyengine/it/dynamic/service/DynamicServiceSqlInjectionTest.java

Comment thread base/src/main/java/com/tinyengine/it/dynamic/dto/DynamicQuery.java Outdated
@chilingling chilingling merged commit caa07a6 into opentiny:develop Apr 25, 2026
1 check 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.

2 participants