SQL 注入(SQL Injection)原理
SQL 注入是一种常见的 Web 安全漏洞 ,攻击者通过在输入字段中注入恶意 SQL 代码,来操纵数据库查询,从而窃取数据、修改数据,甚至破坏数据库。
基本原理:
- 服务器接收用户输入,并将其作为 SQL 语句的一部分执行。
- 如果输入未经过严格检查和过滤,攻击者可以构造特殊输入,使 SQL 语句的逻辑发生变化,从而执行额外的数据库操作。
例如,假设有以下 SQL 查询:
cpp
std::string sql = "SELECT * FROM users WHERE username = '" + username + "' AND password = '" + password + "'";
如果 username
传入:
' OR '1'='1
那么最终 SQL 语句变成:
sql
SELECT * FROM users WHERE username = '' OR '1'='1' AND password = '';
'1'='1'
始终为真,导致 查询返回所有用户,攻击者可以直接绕过登录验证。
SQL 注入攻击示例
示例 1:绕过登录认证
假设有一个 C++ 代码处理用户登录:
cpp
std::string sql = "SELECT * FROM users WHERE username = '" + username + "' AND password = '" + password + "'";
sql::ResultSet* res = mysqlUtil_.executeQuery(sql);
if (res->next()) {
std::cout << "登录成功!" << std::endl;
} else {
std::cout << "用户名或密码错误!" << std::endl;
}
攻击方式:
- 输入用户名 :
admin' --
- 输入密码 :
随便填
最终生成的 SQL 语句:
sql
SELECT * FROM users WHERE username = 'admin' --' AND password = '';
--
使得 后面的AND password = ''
被注释掉 ,攻击者成功绕过密码验证,直接以admin
身份登录。
示例 2:获取数据库中的所有用户
假设攻击者想要获取 users
表中的所有数据,他们可以输入:
- 用户名 :
' OR 1=1 --
- 密码 :
随便填
导致 SQL 变成:
sql
SELECT * FROM users WHERE username = '' OR 1=1 --' AND password = '';
由于 1=1
始终为真 ,这个查询会返回 所有用户的数据 ,攻击者就能获取整个 users
表的信息。
示例 3:修改数据库中的数据
如果数据库允许修改密码:
cpp
std::string sql = "UPDATE users SET password = '" + new_password + "' WHERE username = '" + username + "'";
攻击者输入:
- 用户名 :
admin' --
- 新密码 :
hacked
最终 SQL 语句:
sql
UPDATE users SET password = 'hacked' WHERE username = 'admin' --';
攻击者成功修改了 admin
用户的密码。
示例 4:删除数据库表
如果服务器执行如下 SQL 语句:
cpp
std::string sql = "DELETE FROM users WHERE username = '" + username + "'";
攻击者输入:
- 用户名 :
' OR 1=1 --
导致 SQL 变成:
sql
DELETE FROM users WHERE username = '' OR 1=1 --';
1=1
始终为真 ,结果是 删除 users
表中所有数据!
示例 5:联合查询窃取数据库信息
如果前端显示用户信息:
cpp
std::string sql = "SELECT id, username FROM users WHERE username = '" + username + "'";
攻击者输入:
- 用户名 :
' UNION SELECT id, password FROM users --
最终 SQL 语句:
sql
SELECT id, username FROM users WHERE username = '' UNION SELECT id, password FROM users --';
这样攻击者就能获取 users
表中的 id
和 password
字段!
SQL 注入的防范方法(C++)
C++ 主要使用 MySQL Connector/C++ 或 SQLite 进行数据库操作,防止 SQL 注入的关键在于:
- 使用预处理语句(Prepared Statements)
- 限制数据库权限
- 使用输入过滤
- 隐藏错误信息
- 使用 Web 应用防火墙(WAF)
方法 1:使用预处理语句
预处理语句(Prepared Statement)可以自动处理用户输入,防止 SQL 注入。
✅ 使用 MySQL Connector/C++
预处理语句
cpp
sql::PreparedStatement* pstmt;
pstmt = conn->prepareStatement("SELECT id FROM users WHERE username = ? AND password = ?");
pstmt->setString(1, username);
pstmt->setString(2, password);
sql::ResultSet* res = pstmt->executeQuery();
if (res->next()) {
std::cout << "登录成功!" << std::endl;
} else {
std::cout << "用户名或密码错误!" << std::endl;
}
为什么安全?
?
占位符不会被 SQL 解析为代码,不会拼接 SQL 语句,而是让数据库单独处理参数。setString()
自动转义输入,无论攻击者输入什么,数据库都不会把它当成 SQL 代码执行,而是当成 普通字符串。
方法 2:限制数据库权限
即使攻击者获得了访问权限,也应该 最小化数据库的权限:
sql
GRANT SELECT ON mydb.users TO 'webuser'@'localhost';
REVOKE DELETE, INSERT, UPDATE ON mydb.users FROM 'webuser'@'localhost';
webuser
只能查询数据,不能修改、删除数据。
方法 3:输入过滤
对用户输入进行严格的 白名单过滤:
cpp
bool isValidUsername(const std::string& username) {
return username.find("'") == std::string::npos && username.find("--") == std::string::npos;
}
但这不能完全防止 SQL 注入,最好结合预处理语句!
方法 4:隐藏错误信息
在 SQL 语句执行失败时,不要返回数据库错误信息:
cpp
try {
sql::ResultSet* res = stmt->executeQuery(query);
} catch (sql::SQLException &e) {
std::cout << "系统错误,请稍后再试。" << std::endl;
}
否则攻击者可以通过错误信息 推测数据库结构。
方法 5:使用 Web 应用防火墙(WAF)
ModSecurity 等 WAF 可以检测 SQL 注入攻击,并阻止可疑的 SQL 语句:
sh
apt install libapache2-mod-security2
总结
SQL 注入是一种非常危险的漏洞 ,但可以通过 预处理语句、权限管理、输入过滤等方式防止 :
✅ 使用预处理语句 (防止 SQL 拼接)
✅ 限制数据库权限 (防止数据泄露)
✅ 隐藏错误信息 (防止信息泄露)
✅ 使用 WAF 防火墙(阻止恶意 SQL 语句)
在 C++ 代码中,一定要使用预处理语句,这样才能从根本上防止 SQL 注入!