Save 2H-K/bf8328f70f72196545b0d9589367cb6a to your computer and use it in GitHub Desktop.
Vulnerability Type: CWE-89: Improper Neutralization of Special Elements used in an SQL Command ('SQL Injection')
Specific Issue: Unsafe Query Construction via String Concatenation in ORM Criteria API
Affected Component: drogon::orm::Criteria JSON constructor and drogon::orm::RestfulController::makeCriteria()
Impact: Remote attackers can bypass WHERE conditions and extract arbitrary database data through boolean blind injection via the JSON filter body parameter. Exploitation requires no authentication when the generated RESTful controller is deployed without an authentication filter — which is the default behavior of the drogon_ctl scaffold.
Default Status: The vulnerable data flow is reachable through the standard ORM/REST workflow exposed by the framework. When developers enable restful_api_controllers in model.json (the documented workflow for scaffolding a REST API) and generate the controller via drogon_ctl , the generated get() endpoint accepts an untrusted JSON filter body and processes it through makeCriteria() , which passes operator strings directly to the unsafe Criteria constructor without validation. The generated controller attaches no authentication filter by default ( filters: [] in model.json ), so the endpoint is exposed unauthenticated unless the developer explicitly configures one.
Key Differentiator vs Finding #1: This vulnerability allows WHERE condition bypass (e.g., returning all rows when a restrictive filter should apply), in addition to boolean blind data extraction.
The vulnerable code pattern ( conditionString_.append(json[1].asString()) without operator validation) has existed since the Criteria JSON constructor was first introduced. Exploitation was confirmed on v1.9.13 with the restful_api_controllers scaffold enabled and no authentication filter configured (the default for generated controllers), against PostgreSQL 16.
Introduced In: v1.0.0-beta8
Fixed In: None (as of 2026-05-10)
Tested Version: v1.9.13 (compiled from source against PostgreSQL 16.13)
Note: Source inspection shows identical code across all versions, but "same code pattern" does not guarantee identical exploitability in every version — upstream changes to calling context, default configurations, or type constraints may affect behavior. Exploitation was confirmed only on v1.9.13.
Note: Source inspection shows identical code across all versions, but "same code pattern" does not guarantee identical exploitability in every version — upstream changes to calling context, default configurations, or type constraints may affect behavior. Exploitation was confirmed only on v1.9.13.
3. Technical Analysis
3.1 Source-to-Sink Data Flow
Note: Line numbers refer to the Drogon framework source files ( drogon/drogon_ctl/templates/restful_controller_base_cc.csp for the template and drogon/orm_lib/src/{Criteria,RestfulController}.cc for ORM). Actual generated controller files may have slightly different line numbers due to project‑specific expansions.
Sink — drogon/orm_lib/src/Criteria.cc:90-112 :
Bridge — drogon/orm_lib/src/RestfulController.cc:40-54 :
The Criteria JSON constructor splits the input into three parts: column name ( json[0] ), operator ( json[1] ), and value ( json[2] ). The security treatment is inconsistent:
The operator is the only part that is both attacker-controlled and concatenated into SQL without any validation or parameterization.
3.4 Masquerading Protection Gap
The operator is unprotected regardless of masquerading configuration.
drogon_ctl create model generates REST controllers with [[filters]] = empty string when model.json 's restful_api_controllers.filters is absent or an empty array ( create_model.cc:1498-1513 ). The default generated model.json ships "filters": [] ( model_json.csp:90 ), and restful_api_controllers.enabled defaults to false — controllers are only produced once a developer explicitly opts into the REST scaffold, which is the intended usage for building a REST API.
The JSON filter body parameter is parsed from req->jsonObject() — fully attacker-controlled
The get() endpoint in generated controllers accepts both query parameters and a JSON body simultaneously ( restful_controller_base_cc.csp:300-305 )
Framework ships zero built-in authentication filters — no LoginFilter , AuthFilter exists ( lib/src/ provides only GlobalFilters , IntranetIpFilter , LocalHostFilter )
Unauthenticated exposure confirmed by generated code: the controller generated by drogon_ctl registers get() via METHOD_ADD([[className]]::get,"",Get,Options[[filters]]) ( restful_controller_custom_h.csp:66 ). With the default empty filters , no filter constraint is attached, so the JSON filter body is reachable unauthenticated. Note this applies once the developer has enabled restful_api_controllers ; the controller is not generated by default.
4.1 Environment Setup
System Environment: Ubuntu virtual machine or physical machine
Step 1: Install Required Dependencies
Install the essential build tools, mandatory libraries, and database development libraries according to the Drogon installation guide .
Note: The PostgreSQL client development library must be installed before compiling Drogon, otherwise the ORM will report NO DATABASE FOUND .
Note: The PostgreSQL client development library must be installed before compiling Drogon, otherwise the ORM will report NO DATABASE FOUND .
Step 2: Clone the Drogon Source Repository
Step 3: Enter the Repository
Step 4: Initialize Submodules and Build from Source
Step 5: Verify drogon_ctl is Available
Step 6: Create a New Drogon Project
If you prefer not to install Drogon globally and want source-level debugging, use the path to drogon_ctl:
Then, modify the generated CMakeLists.txt to use local Drogon source directly (minimal changes). out or remove the system-installed Drogon lines and uncomment/enable the local source lines:
Ensure Trantor submodule is initialized: git submodule update --init in the Drogon source directory.
Ensure Trantor submodule is initialized: git submodule update --init in the Drogon source directory.
Step 7: Create a New Drogon Project (Global Install)
Step 7: Prepare the Database Schema
Place the provided schema.sql file (see attachments) in your custom working directory.
Step 8: Start PostgreSQL via Docker
Step 9: Verify Database Tables Were Created
Enter the container and inspect the tables:
Step 10: Configure the Project
Navigate to your Drogon project directory. If config.json does not exist, copy it from the Drogon source tree:
Edit config.json and models/model.json to configure the database connection string and the project listening port. For the debugging phase, it is recommended to remove the following HTTPS listener block:
Additionally, remove the entire redis_clients field, as it is unrelated to the vulnerability.
Step 11: Create main.cc
Create main.cc with the following content:
This is the standard entry point for a Drogon application. The controllers are automatically generated by drogon_ctl, and you do not need to modify them.
Step 12: Generate Controllers and Build
Generate the ORM models and RESTful controllers using drogon_ctl (use the path to your local drogon_ctl if not installed globally):
This command will automatically create:
ORM model classes (Users.h/cc, etc.) in the models/ directory
RESTful controller classes (RestfulUsersCtrl.h/cc, RestfulUsersCtrlBase.h/cc, etc.) in the controllers/ directory
Step 13: Run the Application and Verify
Open your browser and visit (the port configured in config.json , default is 80 ) to confirm the application is running normally.
4.2 Manual Verification — WHERE Condition Bypass
Assuming I configured port 8200 as the startup port
Normal query — id=999 returns empty (no match):
Injected — OR 1=1 tautology returns ALL rows:
Injected — AND 1=0 contradiction returns empty:
4.3 Manual Verification — Boolean Blind Data Extraction
Guess admin password_hash[1] = 'a' (correct) → returns admin row:
Guess admin password_hash[1] = 'z' (wrong) → returns empty:
4.4 Automated Exploit
Full automated exploit script available in attachments: exploit_criteria_sqli.py
Results: Full database extraction successful — same attack phases as Finding #1 (metadata, schema, credentials, financial data).
4.5 Injection Constraints
Confidentiality: HIGH
WHERE condition bypass: OR 1=1 returns all rows regardless of intended filter — bypasses row-level access control (e.g., user_id=current_user_id )
Boolean blind injection: Full database compromise — same extraction capability as Finding #1
Extracted: user credentials, salary data, financial records, business secrets, database metadata
WHERE clause injection typically cannot modify data in the observed attack paths (SQL verb is primarily SELECT)
Stacked queries blocked by PostgreSQL prepared statement mechanism
Malformed payloads cause syntax errors caught by framework (returns HTTP 400/500)
Authorization Bypass:
This is the key differentiator from Finding #1: attackers can bypass WHERE conditions to access data beyond intended access boundaries
Example: if application code filters by user_id=current_user_id , injection with OR 1=1 returns all users' data
6. Proposed Mitigation
Short-term — Add operator whitelist in Criteria.cc:107 :
Migrate WHERE clause construction to use parameterized binding for operators (not just values)
Consider using a query builder pattern that separates operator selection from SQL construction
Add authentication filter support to drogon_ctl create controller CLI
Source Repository:
Sink (Criteria operator injection): drogon/orm_lib/src/Criteria.cc:107 — conditionString_.append(json[1].asString()) For context:
Bridge (RestfulController.cc): drogon/orm_lib/src/RestfulController.cc:40-54 — orm::Criteria(newJson) passes operator unchecked after column name whitelist validation
Unused Defense Function: drogon/orm_lib/inc/drogon/orm/BaseBuilder.h:116 — isValidSqlIdentifier() exists but could be reused for operator validation
PoC Repository: (private, access granted to reviewers)
CWE-89:
The full story
This article is one source in a clustered incident — the cluster page carries the summary, timeline and every other outlet covering it.
