Save 2H-K/33f46c954fd3ed16b32eeddd598f90cf 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 API
Affected Component: drogon::orm::Mapper::orderBy() and drogon_ctl restful controller template
Impact: Remote attackers can extract arbitrary database data through boolean blind injection via the sort query 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 receives the untrusted sort query parameter and passes it directly to the unsafe orderBy() API. 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.
The vulnerable code pattern ( formattedString(" order by %s", colName) without validation) has existed since the restful controller generator 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 (when restful_controller_base_cc.csp template was added)
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/inc/drogon/orm/Mapper.h for ORM). Actual generated controller files may have slightly different line numbers due to project‑specific expansions.
Sink — drogon/orm_lib/inc/drogon/orm/Mapper.h:1980-2002 :
Source — drogon/drogon_ctl/templates/restful_controller_base_cc.csp:243-264 :
The orderBy() method uses utils::formattedString() (a sprintf -like function) to interpolate colName directly into the SQL ORDER BY clause. There is no:
Column name validation — any string is accepted
Parameterized binding — the value is concatenated into the SQL text, not passed as a parameter
Escaping — special characters are not sanitized
A defense function isValidSqlIdentifier() already exists in BaseBuilder.h:116-131 that restricts characters to [a-zA-Z0-9_.] , but no validation step was observed along the analyzed execution path for orderBy() . It is only used in JOIN methods wrapped in assert() (removed in release builds via NDEBUG ).
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.
Framework ships zero built-in authentication filters — no LoginFilter , AuthFilter exists ( lib/src/ provides only GlobalFilters , IntranetIpFilter , LocalHostFilter )
The sort parameter is parsed from req->parameters() (HTTP query string) — fully attacker-controlled
masquerading does not protect the orderBy path (only protects filter/WHERE and create/update)
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 sort parameter 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 include Drogon source directly:
Ensure Trantor submodule is initialized: git submodule update --init in the Drogon source directory.
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
Assuming I configured port 8200 as the startup port
Boolean TRUE — sorts by id (numeric order: 1,2,3,4,5,6,7,8):
Boolean FALSE — sorts by username (alphabetical order: admin,lisi,sunqi,wangwu,wujiu,zhangsan,zhaoliu,zhouba):
Data extraction — guess admin password_hash first character = 'a' (correct):
Data extraction — guess = 'z' (wrong):
4.3 Automated Exploit
Full automated exploit script available in attachments: exploit_orderby_sqli.py
Full attack results (27,883 HTTP requests, 642 seconds):
4.4 Injection Constraints
Confidentiality: HIGH
Full database compromise via boolean blind injection through ORDER BY clause
Extracted: user credentials (password hashes), salary data, financial records, business secrets (product cost prices, supplier names), database metadata (version, internal IP, user privileges)
ORDER BY 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 500)
No denial of service vector
6. Proposed Mitigation
Short-term — Add column name validation in Mapper.h:1980 :
Migrate ORDER BY construction to use parameterized binding where possible
Add authentication filter support to drogon_ctl create controller CLI
Apply isValidSqlIdentifier() validation to all identifier inputs across the ORM layer
Source Repository:
Sink (orderBy injection): drogon/orm_lib/inc/drogon/orm/Mapper.h:1986 — formattedString(" order by %s", colName.c_str()) If you need more surrounding lines for context:
Source (template): drogon/drogon_ctl/templates/restful_controller_base_cc.csp:243-264 — mapper.orderBy(field, SortOrder::ASC) (lines 254, 259, 263)
Unused Defense Function: drogon/orm_lib/inc/drogon/orm/BaseBuilder.h:116 — isValidSqlIdentifier() (exists but was not observed to be called in the analyzed orderBy execution path)
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.
