SQL Injection — 一种针对应用程序数据库的攻击类型,攻击者将恶意SQL代码注入到查询参数中,从而获得对数据的未授权访问或修改数据的能力。根据OWASP (2025)的数据,SQL注入仍然是可能导致数据库完全受损的关键漏洞之一。SQL代码注入允许攻击者读取、修改和删除记录,在某些情况下还可以访问服务器的操作系统。
要点
SQL注入是一种漏洞,当应用程序通过字符串拼接用户数据来构建SQL查询时产生。攻击者向查询参数发送一个特制的字符串,该字符串改变了SQL命令的结构。输入的字符串不再是数据,而是成为SQL代码的一部分,使攻击者能够对数据库执行任意查询。根据Verizon数据泄露报告(2025),SQL注入存在于所有被调查数据泄露的8%中。
尽管该漏洞广为人知(首次提及是在20世纪90年代末),SQL注入仍然出现在现代应用中。原因是人为因素:开发人员在代码中允许字符串拼接,遗留代码未进行重构,ORM框架使用不当(例如,通过字符串替换值的原始查询)。根据Veracode(2026)的数据,大约14%的已扫描应用至少包含一个SQLi漏洞。
成功的SQL注入为攻击者提供了广泛的能力:读取数据库中的任何表,包括密码哈希和用户的个人数据;修改和删除记录;执行管理操作(DROP TABLE, TRUNCATE);在某些配置下——通过xp_cmdshell(MSSQL)或INTO OUTFILE(MySQL)远程执行命令。后果从用户数据泄露到完全丧失对系统的控制。
SQL注入根据从数据库提取数据的方式进行分类。方法的选择取决于应用程序如何处理查询结果和错误消息。OWASP分类区分了三种主要类型:In-band(通过同一通道提取数据)、Inferential/Blind(逻辑推断)和Out-of-band(通过另一通道传输数据)。
| 类型 | 数据提取方式 | 复杂度 | 频率 |
|---|---|---|---|
| In-band(经典) | 直接通过查询结果 | 低 | 高 |
| 盲SQL注入 | 根据服务器响应进行逻辑推断 | 高 | 中 |
| Out-of-band | 通过外部通道(DNS、HTTP) | 中 | 低 |
最常见的类型。攻击者将SQL代码注入到查询参数中,注入结果直接在服务器响应中可见。两个子类型:基于错误的注入(通过数据库错误消息)和UNION查询注入(通过UNION SELECT运算符)。基于错误的注入利用错误消息中的信息,例如可能泄露表名或查询结构的MySQL语法错误。UNION查询注入允许将合法查询的结果与数据库中其他表的数据合并。
当应用程序不显示查询结果或错误消息时使用。攻击者通过发送带有逻辑条件的查询并分析服务器响应的差异(例如响应时间或页面内容)来提出“是/否”问题。基于时间的盲SQL注入使用延迟函数(SLEEP, WAITFOR DELAY)来确认条件——如果页面加载时间更长,则条件为真。这种方法非常缓慢——提取一条记录可能需要数小时。
# 盲SQL注入示例(基于时间)
# 如果存在SQLi漏洞,SLEEP(2)在条件满足时执行
import requests
import time
payload = "' OR IF(SUBSTRING((SELECT password FROM users LIMIT 1),1,1)='a',SLEEP(2),0) -- "
url = "http://example.com/user?id=1" + payload
start = time.time()
response = requests.get(url)
elapsed = time.time() - start
# 如果响应在>2秒后到达——密码的第一个字母是'a'
print(f"Time: {elapsed:.2f}s - First letter is 'a'" if elapsed > 2 else "First letter is not 'a'")
数据不是通过HTTP响应传输,而是通过替代通道:DNS查询、对外部服务器的HTTP请求、SMTP。当应用程序不返回查询结果且不显示错误时使用。MySQL支持LOAD_FILE()函数,该函数可以发起DNS查询,MSSQL支持xp_dirtree函数用于将数据发送到远程SMB服务器。Out-of-band SQL注入有效,但需要在攻击者的服务器上进行额外配置,并且需要特定的数据库功能存在。
SQL注入的机制基于SQL使用引号表示字符串字面量。如果应用程序将用户输入直接替换到SQL查询中而不进行转义,攻击者可以“关闭”字符串并添加任意SQL代码。例如,在查询SELECT * FROM users WHERE name = '$input'中,替换' OR '1'='1将查询变为SELECT * FROM users WHERE name = '' OR '1'='1',这将返回所有用户。
考虑一个带有查询SELECT * FROM users WHERE username = '$user' AND password = '$pass'的登录表单。如果攻击者在用户名字段中输入admin' -- 并将密码留空,结果查询变为SELECT * FROM users WHERE username = 'admin' -- ' AND password = ''。--字符注释掉了查询的其余部分,密码检查条件被禁用。服务器返回管理员用户的记录,攻击者在不知晓密码的情况下进入系统。
# SQL注入示例——绕过身份验证
# 脆弱代码:直接字符串拼接
def login_vulnerable(username, password):
query = f"SELECT * FROM users WHERE username='{username}' AND password='{password}'"
cursor.execute(query)
return cursor.fetchone() is not None
# username = "admin' --" 取消密码检查
# 安全版本:参数化查询
def login_secure(username, password):
query = "SELECT * FROM users WHERE username = %s AND password = %s"
cursor.execute(query, (username, password))
return cursor.fetchone() is not None
UNION SELECT运算符允许合并两个SELECT查询的结果。如果攻击者找到脆弱的参数,他添加一个带有来自其他表的查询的UNION SELECT。例如:' UNION SELECT username, password FROM admins --。成功的条件是——两个查询中的列数必须相同。列数通过ORDER BY确定(替换' ORDER BY 1--,然后2、3...直到出错)。知道列数后,攻击者替换具有相同字段数的UNION SELECT。
移动应用在服务器端(API)和客户端——本地数据库(SQLite, Realm)中都会遇到SQL注入。虽然移动API中的服务器端SQL注入风险与Web应用类似,但本地数据库创建了额外的攻击向量。如果应用将数据存储在SQLite中并通过字符串拼接执行查询,通过API进入本地数据库的恶意数据可能在后续处理中导致SQL注入。
设备上的本地SQLite数据库也容易受到SQL注入的影响,如果应用通过字符串拼接构建查询。Android上的Content Providers和iOS上的Core Data默认使用参数化,但原始查询需要开发人员的注意。SQLite不支持通过分号执行多个查询,这限制了攻击者的能力,但不能防止通过WHERE条件读取数据。在Android上始终使用selectionArgs,在iOS上使用带参数的NSPredicate。
移动应用连接的API与任何Web服务器一样脆弱。移动开发人员通常认为SQLi只是后端的问题,但漏洞出现在接受客户端参数的API端点级别。责任分离不能提供保护:如果后端开发人员忘记参数化查询,用户的移动应用就变成了攻击向量。要求后端使用ORM或预编译语句。
// Android本地SQLite中的SQL注入示例
// 脆弱代码:直接拼接
fun getUserVulnerable(db: SQLiteDatabase, userId: String): Cursor {
return db.rawQuery(
"SELECT * FROM users WHERE id = $userId", null
)
}
// 安全版本:通过selectionArgs参数化
fun getUserSecure(db: SQLiteDatabase, userId: String): Cursor {
return db.rawQuery(
"SELECT * FROM users WHERE id = ?",
arrayOf(userId)
)
}
防御SQL注入基于一个简单的原则:永远不要信任SQL查询中的用户输入。唯一可靠的方法是查询参数化(预编译语句),其中SQL代码和数据分开传输。所有其他方法——转义、验证、WAF——是额外的保护层,但不能替代参数化。根据OWASP(2025)的数据,参数化可以防止100%的SQL注入攻击。
使用预编译语句时,SQL查询首先由数据库服务器编译而不带数据,然后参数值分开传输。数据库将参数视为数据,而不是可执行代码。即使攻击者发送' OR '1'='1,数据库也会将其解释为字符串值,而不是SQL代码。所有现代语言和框架都支持预编译语句:PHP中的PDO、Java中的PreparedStatement、Python中的cursor.execute。
现代ORM(Hibernate, Entity Framework, SQLAlchemy, Room)在执行查询时自动使用参数化,除非开发人员切换到原始查询。然而,ORM不能完全保护:JPA中诸如@Query(value = "SELECT * FROM users WHERE name = :name", nativeQuery = true)之类的构造需要通过命名参数传递参数,而不是通过拼接。查询构建器(Knex, jOOQ)默认也会参数化查询,如果不使用原始方法的话。
转义特殊字符(mysql_real_escape_string)——一种过时的方法,不能防御所有类型的SQL注入。问题:转义依赖于编码,可以通过使用多字节编码绕过(例如,亚洲系统中的GBK)。仅在不允许参数化的遗留代码中使用转义,并且始终与严格的数据类型验证结合使用。
| 方法 | 有效性 | 建议 |
|---|---|---|
| 预编译语句 | 100% | 所有查询必须使用 |
| ORM(正确使用) | 99% | 推荐 |
| 字符串转义 | 70%(取决于编码) | 仅遗留代码 |
| 输入验证(白名单) | 50%(仅适用于数字) | 辅助 |
| WAF(Web应用防火墙) | 60% | 辅助 |
应用程序账户应具有最低必要权限:SELECT、INSERT、UPDATE、DELETE——仅限应用程序实际需要的表。禁止应用程序账户使用DROP、TRUNCATE、CREATE。这将限制即使成功SQL注入造成的损害:攻击者无法删除表或执行管理操作。
定期进行SQL注入测试应成为安全开发流水线的一部分。静态分析、动态扫描和手动渗透测试的结合能提供最佳结果。根据Synopsys网络安全报告(2025),自动扫描器可以发现高达70%的SQLi漏洞,但复杂的盲注攻击需要手动测试。
对于移动应用,本地SQLite分析也很重要:检查所有rawQuery调用、ContentProvider查询以及带有rawQuery的Room查询。工具:Android Studio Lint(检测SQLite中的SQLi),MobSF(移动安全框架)用于APK/IPA的自动静态和动态分析。还建议通过带有移动应用流量代理拦截的sqlmap测试API端点。
常见问题
SQL注入通过SQL查询攻击关系型数据库。NoSQL注入通过查询操作符($gte, $ne, $where)影响非关系型数据库(MongoDB, Couchbase)。在MongoDB中,如果应用程序从JSON字符串构建BSON文档,则可能发生注入。保护机制类似:参数化和类型验证。
找到所有通过字符串拼接用户数据构建SQL查询的地方。寻找像"SELECT ... WHERE id = " + userId或f"UPDATE ... SET name = '{name}'"这样的模式。每个这样的字符串都是潜在的SQL注入。全部替换为参数化查询或预编译语句。
ORM框架只有在使用它们的查询构建器方法和命名参数时才能自动保护。如果使用原始查询(JPA中的nativeQuery,Room中的rawQuery),保护无效——必须通过准备好的表达式传递参数,而不是通过字符串拼接。
二次SQL注入是一种攻击,其中恶意数据作为安全数据存储在数据库中,但随后在另一个查询中未经转义就被使用。例如,攻击者使用用户名' OR '1'='1注册。数据作为字符串存储——注册时没有攻击。但如果另一个查询将用户名替换到SQL中而不进行参数化,注入就会被激活。
NoSQL注入由于开发人员意识较低而可能更危险。开发人员了解SQL注入,大多数人使用ORM,但很少有人知道NoSQL注入。在MongoDB中,格式错误的查询可能返回集合中的所有文档。保护——相同的预编译语句(BSON参数化)和严格的输入数据验证。
总结
我们将开发一款交钥匙移动应用程序
IT Sectr自2017年以来为初创企业和企业打造iOS和Android应用程序。我们将为您提供咨询并提出最佳解决方案。