首先,我完全意识到SQL注入漏洞,我正在使用PDO为我在PHP开发的新应用程序。
长话短说,我工作的组织目前无法承担任何人力资源将我目前正在开发的相当大的应用程序的所有内容切换到PDO,因此我在此期间坚持使用mysql_*
函数。
无论如何,我想知道使用数据验证函数来"消毒"内插查询中使用的数字参数是否安全。我们确实对字符串使用mysql_real_escape_string()
(是的,我也意识到那里的局限性)。下面是一个例子:
public function foo($id) {
$sql = "SELECT * FROM items WHERE item_id = $id";
$this->query($sql); // call mysql_query and does things with result
}
$id
id用户通过HTTP GET提供的值,所以很明显这个代码是脆弱的。我这么做可以吗?
public function foo($id) {
if (!ctype_digit($id)) {
throw new 'InvalidArgumentException("ID must be numeric");
}
$sql = "SELECT * FROM items WHERE item_id = $id";
$this->query($sql); // call mysql_query and does things with result
}
据我所知,ctype_digit
与检查'd+
的正则表达式相同。
(也有filter_var($id, FILTER_VALIDATE_INT)
,但这可能会返回int(0)
,在松散类型比较下计算为FALSE
,所以我必须在那里做=== FALSE
。)
更新:
- 变量不仅包括主键,还包括任何类型为
boolean
,tinyint
,int
,bigint
等的字段,这意味着零是一个完全可以接受的搜索值。我们正在使用PHP 5.3.2
是的,如果您确实虔诚地使用正确的函数来验证数据并正确地阻止查询运行,如果数据不像预期的那样,我可以看到没有漏洞。ctype_digit
有一个非常有限和明确的目的:
如果字符串文本中的每个字符都是十进制数字则返回
TRUE
,否则返回FALSE
。
这个函数基本上不会出错,所以使用它是安全的。它甚至会以空字符串返回false
(从PHP 5.1开始)。注意,is_numeric
不是那么值得信赖。我可能仍然会添加一个范围检查,以确保数字在预期范围内,我不确定整数溢出会发生什么。如果在此检查之后再强制转换为(int)
,则没有注入的机会。
注意事项:与所有非本机参数化查询一样,如果您对连接字符集进行了任何恶作剧,仍然有机会注入。可能通过的字节范围受到ctype_digit
的严格限制,但您永远不知道会出现什么。
是的,它会工作。如果值不是一个数字字符串,你的代码将引发一个异常,你只需要捕获它并向用户显示一个错误消息。
注意ctype_digit($foo)
:
- 如果
$foo
在PHP 5.1之前为空,则返回true
(参见文档); - 将返回
$foo
的int
值在[48, 57]
间隔(ASCII数字)之外的所有false
。
所以如果你打算使用ctype_digit($foo)
$foo
是不是一个非空的string
我不明白这里有什么问题。根据您发布的代码,您已经在使用某种DB包装器,并且已经计划更改每个数字参数的调用代码。为什么不修改DB包装器使其支持预处理语句,并修改调用代码以使用它呢?长话短说,我所在的组织目前不能委派任何人力资源来把所有事情都交给PDO。
旧的mysql ext不是问题-可以用它来模拟准备好的语句。
我完全意识到SQL注入漏洞。
你的"全意识"有点夸张了。不幸的是,大多数人并不了解真正的注入源,以及准备好的语句的真正目的。
将数据从查询中分离出来是一个很好的技巧,但是完全没有必要。而预处理语句的真正价值在于它的必然性,而不是手工格式化的本质随意性。
你的另一个错误是对字符串的分离处理——部分在查询中格式化(添加引号),部分在查询外格式化(转义特殊字符),这也是一个调用灾难。
既然你决定坚持手动格式化,那么享受你的注射,迟早的事。你的想法适用于人工的、完全可控的沙盒例子。然而,在现实生活中的应用程序中,情况就不一样了,因为有很多人在开发它。不是让程序格式化你的数据,而是让人去做。后果很明显。
这让我想知道为什么PHP用户不能从他们的错误中吸取教训,并且仍然急切地设计实践,这些实践在很久以前就被证明是不可靠的。
刚刚发现了你推理中的另一个谬误
是用户通过HTTP GET提供的值,所以很明显这个代码是脆弱的。
您必须理解任何未格式化的值都会使此代码容易受到攻击,无论是HTTP GET, FTP PUT还是文件读取。不仅臭名昭著的"用户输入"必须正确格式化,而且任何输入都必须正确格式化。这就是为什么必须使DB驱动程序成为格式化发生的唯一位置。格式化数据的应该是程序而不是开发人员。你的想法与这样一个基石原则相矛盾。
使用mysql_real_escape_string
并将$id
用单引号括起来。单引号保证了安全性,避免了sql注入的可能性。
例如,SELECT * FROM table WHERE id = 'escaped string'
不能被黑为:SELECT * FROM table WHERE id = 1; DROP table;
,因为'1; DROP table;'
将被视为WHERE
的输入参数。
ctype_digit()
将对$id
的大多数整数值返回false。如果要使用该函数,请先将其强制转换为string:
public function foo($id) {
$id = (string)$id;
if (!ctype_digit($id)) {
throw new 'InvalidArgumentException("ID must be numeric");
}
$sql = "SELECT * FROM items WHERE item_id = $id";
$this->query($sql); // call mysql_query and does things with result
}
这是因为整型被解释为ASCII值。
我在简单的情况下使用intval()
,尽管(int)
显然占用更少的资源。例子:
$sql =
"SELECT * FROM categories WHERE category_id = " .
intval($_POST['id']) .
" LIMIT 1";