将带有修改的字段名称和白名单验证的 SQL 语句传递给 RPC 函数。它安全且普遍吗?
Posted
技术标签:
【中文标题】将带有修改的字段名称和白名单验证的 SQL 语句传递给 RPC 函数。它安全且普遍吗?【英文标题】:Passing SQL statements with modified field names and whitelist validation to an RPC function. Is it safe and a common practice? 【发布时间】:2019-08-30 20:35:57 【问题描述】:我创建了一个 API,其中可以使用 JSON-RPC 将 SQL 语句作为参数传递。 API 提供了一个表名和列名字典,因此客户端不知道实际的数据库表名和列名,并且可以使用 GET 方法访问。可以使用 POST 方法访问实际的 API 功能。
我使用令牌和正则表达式验证只允许具有有效令牌的请求,只允许某些字符以防止 SQL 注入,并防止使用“drop, insert, into, update”。只有“select”语句是可能的,但在需要时可以允许指定用户使用其他语句。
我的问题是,这是一种安全的过程调用方法吗?如果是这样,这是一种常见的做法吗?我尝试使用浏览器和 Postman 访问 API,它似乎可以工作。不过,我主要关心的是安全性。
以下是 Postman JSON-RPC 请求正文示例。
"jsonrpc": "2.0", "method": "rpc_ql", "params": "token" : "12345", "query" : "select person_id, person_name, person_gender from persons where person_id = 123", "id": 1
/**
* RPC-QL - RPC-QL is a query language based on SQL and accessed
* - over remote procedure call (RPC) API.
*
* @package RPC-QL (SQL on JSON-RPC)
* @author Raymund John Ang <raymund@open-nis.org>
* @license MIT License
*/
// Process POSTed JSON data using JSON-RPC 2.0 specification
$data = json_decode( file_get_contents('php://input'), true );
$jsonrpc = $data['jsonrpc'];
$method = $data['method'];
$params = $data['params'];
$id = $data['id'];
// Remote API function
function rpc_ql()
global $jsonrpc;
global $method;
global $params;
global $id;
// Table and column fields dictionary
$dictionary = [['Table' => 'persons',
'Fields' => ['person_id' => 'integer - ID number of the person',
'person_name' => 'string - Name of the person',
'person_gender' => 'string - Male or Female',
'person_birth_date' => 'string - format YYYY-MM-DD']],
['Table' => 'places',
'Fields' => ['place_id' => 'integer - ID number of the place',
'place_name' => 'string - Name of the place',
'place_state' => 'string - State where the place is located']]];
// Show dictionary with GET request.
if ( $_SERVER['REQUEST_METHOD'] == 'GET' ) echo json_encode($dictionary);
// Access API functionality with POST request.
if ( $_SERVER['REQUEST_METHOD'] == 'POST' )
// Require parameters based on JSON-RPC 2.0 and SQL
if ( empty($jsonrpc) || empty($method) || empty($params['token']) || empty($params['query']) || empty($id) ) exit('Please set "jsonrpc", "method", "token" and "query" parameters, and request "ID".');
$token = $params['token'];
$query = $params['query'];
// Token validation - token should be 12345
if ( ! hash_equals( hash('sha256', 12345), hash('sha256', $token) ) ) exit('Token authentication failed');
// Query validation - query should be alphanumeric with a few accepted characters and blacklisted SQL commands.
if ( preg_match('/^[a-zA-Z0-9 _,*=()\']+$/i', $query) && ! preg_match('/(drop|insert|into|update)/i', $query) )
// Table and column fields converter
$mod_query = str_replace( 'persons', 'db_persons', $query ); // Persons table; Initial conversion of $query.
$mod_query = str_replace( 'person_id', 'db_person_id', $mod_query ); // Succeeding conversion of $mod_query.
$mod_query = str_replace( 'person_name', 'db_person_name', $mod_query );
$mod_query = str_replace( 'person_gender', 'db_person_gender', $mod_query );
$mod_query = str_replace( 'person_birth_date', 'db_person_birthdate', $mod_query );
$mod_query = str_replace( 'places', 'db_places', $mod_query ); // Places table
$mod_query = str_replace( 'place_id', 'db_place_id', $mod_query );
$mod_query = str_replace( 'place_name', 'db_place_name', $mod_query );
$mod_query = str_replace( 'place_state', 'db_place_state', $mod_query );
// Set $error to null if $mod_query contains a query.
if ( $mod_query !== false ) $error = null;
$response = ['jsonrpc' => $jsonrpc, 'result' => $mod_query, 'error' => $error, 'id' => $id];
echo json_encode($response);
else
// Failed validation
$error_message = "The query is not valid. It should only contain alphanumeric characters, spaces, '_', ',', '*', '=', '(', ')' and '''. Only SELECT statements are allowed.";
$response = ['jsonrpc' => $jsonrpc, 'result' => null, 'error' => ['code' => 32600, 'message' => $error_message], 'id' => $id];
echo json_encode($response);
// Execute method if function exists
if ( function_exists($method) )
return $method();
else
$error_message = 'Sorry. The included method does not exist.';
$response = ['jsonrpc' => $jsonrpc, 'result' => null, 'error' => ['code' => 32602, 'message' => $error_message], 'id' => $id];
echo json_encode($response);
【问题讨论】:
我不会这么说的。除非你加密你的流量,否则任何拦截你的 JSON 的人都会泄露你所有的数据库表和模式,这只不过是信息泄漏。如果您在前端和后端 (WAS) 服务器之间使用它,那很好,但仍然很尴尬 嗨@mangusta。流量应该是 HTTPS。客户端将使用的表和列字段名称与数据库不同。有一个转换器可以将客户端键入的字段名称转换为 API 服务器中的实际数据库表和列名称。客户端只能访问白名单字典以供参考,而不是数据库中的实际字段名称。 【参考方案1】:简而言之:没有。
即使您尽可能安全,这种方法仍然存在安全风险。一个非常简单的示例,说明您当前的代码将如何中断:
truncate table persons
这将有效地删除表db_persons
中的所有记录。
只要您允许来自客户端的任何输入直接包含在数据库查询中,您的系统迟早会被破坏。
【讨论】:
谢谢@Sergey。我重构了代码并使用了白名单,而不是黑名单。使用 API 的客户端现在只能使用字典中的术语,并使用 query_data 参数执行 PHP PDO 与?作为占位符。修改后的代码可以访问:github.com/ray-ang/rpc-ql/blob/master/rpc-ql.php 您可以在 POST 请求的请求正文中使用此 JSON 数据来测试 API 将如何处理请求。然后可以将生成的 SQL 语句和 query_data 参数集成到 PDO 语句中。我希望这种方法比以前的方法有更好的安全性。"jsonrpc": "2.0", "method": "rpc_ql", "params": "token" : "12345", "query" : "SELECT person_id, person_name, person_gender, person_birthdate FROM persons WHERE person_id LIKE '%?%' AND person_name LIKE '%?%'", "query_data" : [100, "ete"], "id": 1
嗨@Ray。我检查了您的代码,它看起来确实更好(意思是,我没有立即看到破解它的方法)。但是,我的观点是,您在确保方法安全方面投入的时间和精力越多,您实际上就越接近现代数据库驱动程序已经所做的事情,例如参数化查询。那么为什么要花双倍的时间重新实现已经存在的功能呢?
嗨@Sergey。您仍然需要使用 PDO 和“?”作为占位符,特别是在第 110 行,并使用 JSON-RPC v2.0 使用 $params[query_data] 来保存来自客户端的值。然后将第 112 行 'result' => $mod_query
中的结果更改为实际结果变量。我想实现一种查询语言,就像 GraphQL 一样,API 客户端可以安全地使用 SQL 语句,而不是从 GraphQL 学习新的语句约定。这就是我编写脚本的目标。感谢您强调黑名单验证的危险。
嗨@Ray,在这种情况下 - 祝你的项目好运!如果您计划为查询创建一个完全通用的库,我还建议您检查语言解析器生成器工具,例如 gnu.org/software/bison - 它们可能允许您更轻松地创建非常复杂的验证和解析例程。跨度>
以上是关于将带有修改的字段名称和白名单验证的 SQL 语句传递给 RPC 函数。它安全且普遍吗?的主要内容,如果未能解决你的问题,请参考以下文章