1. 项目概述为什么ASP防注入代码需要“通用”且“最新”十多年前我刚开始接触Web开发时ASPActive Server Pages还是构建动态网站的主流技术。那时候SQL注入攻击已经非常猖獗网上流传着各种“万能防注入代码”。但这么多年过去了为什么我们还在讨论“最新通用ASP防SQL注入代码”原因很简单攻击技术在进化而很多老旧的防御代码早已失效甚至本身就成了安全漏洞。很多遗留的ASP系统仍在线上运行维护者可能并非专业安全人员他们需要的不是一套复杂的安全框架而是一段拿来就能用、能覆盖大部分常见攻击手法的“加固补丁”。所谓“通用”意味着这段代码不依赖于特定的数据库无论是Access、SQL Server还是MySQL也不依赖于特定的表单提交方式GET、POST、Cookies更不绑定于某个具体的业务逻辑。它应该像一层过滤网部署在应用程序的入口处对所有用户输入进行统一的、标准化的清洗。而“最新”则要求它能应对当下主流的、甚至是一些变种的SQL注入手法比如宽字节注入、二次编码注入、HTTP参数污染等这些是老一代防御代码的盲区。这篇文章我将结合自己多年在安全审计和渗透测试中遇到的真实案例拆解一套经过实战检验的ASP防注入代码。我不会只给你一段冰冷的代码而是会详细解释每一行代码背后的防御逻辑、它试图拦截的攻击载荷以及在实际部署中可能遇到的“坑”。无论你是维护一个古老的ASP企业站还是出于学习目的想了解Web安全的基础这篇文章都能给你提供一套可直接部署的解决方案和深入的理解。2. 核心防御思路与方案设计2.1 从攻击者视角理解SQL注入的本质要写好防御代码必须先站在攻击者的角度思考。SQL注入的核心是攻击者通过构造特殊的输入改变了应用程序原本要执行的SQL语句的逻辑结构。一个经典的漏洞代码示例如下% dim username, sql username Request.QueryString(“username”) ‘ 直接从URL获取参数 sql “SELECT * FROM users WHERE username ‘“ username “‘“ ‘ 如果用户输入 admin’ OR ‘1’‘1那么SQL语句就变成了 ‘ SELECT * FROM users WHERE username ‘admin’ OR ‘1’‘1‘ ‘ 这将导致条件永远为真返回所有用户数据。 %攻击者的目标就是插入‘、--、;、UNION、SELECT、UPDATE等这些能改变SQL语义的关键字符或关键字。因此防御的核心思路就两个过滤危险字符和参数化查询。由于ASP原生对参数化查询的支持不如.NET等现代框架友好尤其对于动态拼接的复杂SQL因此在无法全面改造旧系统的情况下一个强大的输入过滤层是性价比最高的选择。2.2 “通用”过滤器的设计原则一个通用的过滤器需要遵循以下几个原则全面性必须覆盖所有可能的输入源。在ASP中用户输入不仅来自Request.FormPOST表单和Request.QueryStringGET参数还可能来自Request.Cookies、Request.ServerVariables如HTTP头等。攻击者可能从任何一点进行突破。深度防御不能只做简单的关键字替换。例如将SELECT替换为空攻击者可能会用SELSELECTECT来绕过如果替换逻辑是简单的删除一次。我们需要更智能的模式匹配。最小干扰过滤规则应尽可能精准避免误杀正常的用户输入。例如用户公司名恰好叫“Union Technologies”如果粗暴地拦截“union”就会影响正常业务。性能考量过滤逻辑应在服务器端执行并且要高效。过于复杂的正则表达式可能会在访问量大的时候成为性能瓶颈。基于这些原则我们的方案是创建一个独立的SQLInjectionFilter.asp文件在其中定义一个核心的过滤函数。在网站全局入口文件如Global.asa的Application_OnStart或Session_OnStart或每个需要防护的页面头部引入并调用这个过滤函数对Request对象的所有集合进行遍历和清洗。3. 核心过滤函数代码逐行解析下面是我在多个项目中打磨后形成的一个核心过滤函数。它不追求用最复杂的正则表达式一网打尽而是在有效性、性能和可维护性之间取得平衡。% ‘ 文件名 SQLInjectionFilter.asp ‘ 功能 通用SQL注入与XSS过滤函数 Function SafeRequest(key, requestType) ‘ key: 参数名 ‘ requestType: 输入类型如 “Get”, “Post”, “Cookie” ‘ 返回值 过滤后的安全字符串 Dim value, tempValue value “” ‘ 1. 根据类型获取原始值 Select Case UCase(requestType) Case “GET” value Request.QueryString(key) Case “POST” value Request.Form(key) Case “COOKIE” value Request.Cookies(key) Case Else ‘ 默认从QueryString和Form中获取兼容旧写法但不推荐 value Request(key) End Select If IsNull(value) Or Trim(value) ““ Then SafeRequest ““ Exit Function End If tempValue Trim(value) ‘ 去除首尾空格 ‘ 2. 一级过滤直接拦截明显恶意攻击优先级最高 ‘ 注意这里使用 InStr 进行不区分大小写的匹配提高拦截效率 Dim dangerPatterns, i dangerPatterns Array(“exec ”, “execute ”, “xp_cmdshell”, “net user”, “net localgroup”, “declare ”, “master..”, “sysobjects”, “syscolumns”, “;”, “--”, “/*”, “*/”, “version”, “char(”, “nchar(”, “waitfor delay ”) For i 0 To UBound(dangerPatterns) ‘ InStr返回位置大于0即表示包含 If InStr(1, tempValue, dangerPatterns(i), vbTextCompare) 0 Then ‘ 记录日志便于后续分析攻击行为 Application.Lock ‘ 简单日志示例实际应写入文件或数据库 ‘ LogAttack “SQLInjection”, requestType, key, tempValue, Request.ServerVariables(“REMOTE_ADDR”) Application.Unlock ‘ 直接返回空或自定义错误信息阻断请求 SafeRequest ““ Exit Function End If Next ‘ 3. 二级过滤处理SQL注入关键字符使用正则表达式更精确 Dim regEx, matches Set regEx New RegExp regEx.IgnoreCase True ‘ 忽略大小写 regEx.Global True ‘ 全局匹配 ‘ 规则1过滤单引号将其转义为两个单引号SQL标准转义方式 regEx.Pattern “‘“ tempValue regEx.Replace(tempValue, “‘‘“) ‘ 注意这里是两个单引号不是一个双引号 ‘ 规则2过滤或报警潜在的联合查询、数据定义等关键字可根据业务放宽 ‘ 这里采用更严格的模式单词边界匹配减少误伤。 ‘ \b 匹配单词边界确保匹配的是独立的单词而不是某个单词的一部分如“unionize” regEx.Pattern “\b(union|select|insert|update|delete|drop|alter|create|truncate|grant|exec|execute|xp_)\b” If regEx.Test(tempValue) Then ‘ 对于关键字一种策略是记录日志并报警另一种是直接过滤掉。 ‘ 这里演示直接过滤掉替换为空可根据安全等级调整。 tempValue regEx.Replace(tempValue, ““) ‘ 强烈建议在此处记录日志 End If ‘ 规则3过滤常见的SQL注入测试载荷如 ‘ or ‘1’‘1 的各种变体 ‘ 匹配诸如 [空格]or[空格]‘1’‘1 的模式 regEx.Pattern “(\sor\s[‘\“]?[01][‘\“]?\s*[!]\s*[‘\“]?[01][‘\“]?)” tempValue regEx.Replace(tempValue, ““) ‘ 4. 可选但推荐基础XSS过滤防止注入的脚本通过数据库再次输出时执行 regEx.Pattern “script[^]*.*?/script” tempValue regEx.Replace(tempValue, ““) regEx.Pattern “javascript:” tempValue regEx.Replace(tempValue, ““) regEx.Pattern “onerror|onload|onclick|onmouseover\s*” tempValue regEx.Replace(tempValue, ““) Set regEx Nothing ‘ 释放对象 ‘ 5. 长度限制辅助防御 ‘ 对某些关键字段如用户名、搜索词进行长度限制能有效增加攻击者构造复杂Payload的难度 If Len(tempValue) 255 Then ‘ 长度阈值根据具体字段调整 tempValue Left(tempValue, 255) End If SafeRequest tempValue End Function ‘ 辅助函数遍历并净化所有输入 Sub FilterAllInput() Dim item, dict ‘ 处理QueryString For Each item In Request.QueryString Request.QueryString(item) SafeRequest(item, “Get”) Next ‘ 处理Form For Each item In Request.Form Request.Form(item) SafeRequest(item, “Post”) Next ‘ 处理Cookies需谨慎可能影响会话 ‘ For Each item In Request.Cookies ‘ Request.Cookies(item) SafeRequest(item, “Cookie”) ‘ Next End Sub %注意直接修改Request.QueryString和Request.Form集合在某些ASP环境下可能不被允许或行为不一致。更稳妥的做法是在使用参数时调用SafeRequest函数获取过滤后的值而不是直接修改Request对象。上面的FilterAllInput子过程主要展示了遍历的思想实际应用时建议采用前者。3.1 代码关键点解析与避坑指南单引号转义是基石regEx.Pattern “‘“和tempValue regEx.Replace(tempValue, “‘‘“)这一行是整个防御的基石。在SQL中单引号是字符串的边界。将其转义为两个单引号意味着用户输入的任何单引号都会在SQL语句中被当作普通字符处理从而无法“逃逸”出字符串的界限。这是防止绝大多数注入攻击最有效的一步。务必注意替换后的字符串是两个连续的单引号不是一个双引号。在SQL中‘‘表示一个单引号字符本身。区分“拦截”与“过滤”代码中分为两级。第一级dangerPatterns数组里的内容如xp_cmdshell、--一旦发现直接返回空并记录日志。这是因为这些模式几乎100%是恶意攻击行为没有误报的可能。第二级对union、select等关键字进行过滤替换为空则需要谨慎。如果你的网站搜索功能需要用户输入“union”这个单词怎么办这就是“最小干扰”原则的挑战。对于这种情况更好的做法是白名单校验对于已知的、格式固定的字段如手机号、邮箱使用严格的正则表达式白名单验证只允许通过符合格式的字符。上下文相关过滤在业务逻辑层判断。如果当前执行的SQL是查询语句且参数本应是数字ID那么任何非数字字符都应被拒绝。记录而非阻断对于疑似攻击但可能是正常输入的情况记录日志并报警由管理员复核而不是直接阻断正常用户。正则表达式的使用技巧\b(union|select...)\b中的\b是“单词边界”元字符。它能确保匹配的是独立的单词“union”而不是“reunion”或“unionized”的一部分。这极大地减少了误杀。这是很多老旧防御代码没有做到的细节。性能与对象释放Set regEx Nothing在函数结束时释放正则表达式对象这是一个良好的编程习惯有助于在长时间运行的ASP应用中管理资源。4. 实战部署与集成方案有了核心函数下一步就是把它集成到你的ASP应用中。这里提供几种方案从简单到全面。4.1 方案一页面级防护快速修补对于无法改动全局配置的虚拟主机这是最常用的方法。在每个需要防护的ASP页面顶部% LanguageVBScript %之后加入以下代码!--#include file“SQLInjectionFilter.asp”-- % ‘ 调用过滤函数获取安全参数 Dim safeUsername, safePassword safeUsername SafeRequest(“username”, “Post”) ‘ 假设从表单提交 safePassword SafeRequest(“pwd”, “Post”) ‘ 然后使用 safeUsername 和 safePassword 进行数据库操作 Dim sql sql “SELECT * FROM users WHERE username ‘“ safeUsername “‘ AND password ‘“ safePassword “‘“ ‘ ... 执行SQL ... %注意事项确保SQLInjectionFilter.asp文件的路径正确。必须所有接收用户输入的页面都进行包含否则攻击者会寻找那个遗漏的页面进行攻击。这种方法工作量大容易遗漏。4.2 方案二全局防护 via Global.asa推荐如果你对网站根目录有控制权可以使用Global.asa文件。在Global.asa的Application_OnStart或Session_OnStart事件中将过滤函数加载到公共区域。但更优雅的方式是利用一个全局包含文件。创建一个include.asp里面包含过滤函数和一些通用设置。然后修改所有页面的顶部将原来的!--#include file“xxx.asp”--统一改为包含这个include.asp。在这个include.asp中你可以自动调用过滤逻辑。‘ 文件 /include/global_functions.asp !--#include file“SQLInjectionFilter.asp”-- % Sub Application_Initialize() ‘ 这里可以初始化一些应用级变量 End Sub ‘ 自动安全请求函数替代直接的Request Function GetSafe(paramName) ‘ 智能判断来源优先Post其次Get If Request.Form(paramName) ““ Then GetSafe SafeRequest(paramName, “Post”) ElseIf Request.QueryString(paramName) ““ Then GetSafe SafeRequest(paramName, “Get”) Else GetSafe ““ End If End Function ‘ 在文件末尾可以执行一些全局过滤需根据环境测试 ‘ Call FilterAllInput() ‘ 谨慎使用 %然后在每个页面顶部包含它!--#include virtual“/include/global_functions.asp”-- % Dim uid uid GetSafe(“id”) ‘ 安全地获取参数 %4.3 方案三IIS 模块或URL重写高级对于Windows Server可以考虑编写一个原生的IIS模块ISAPI Filter或在IIS的URL重写模块中编写规则来过滤请求。这种方法性能最好对代码无侵入性但实现难度最高需要C/C或熟练的IIS配置知识。通常用于企业级防护不适合快速部署。5. 绕过分析与进阶防御没有任何一段过滤代码是绝对无敌的。攻击者总是在寻找绕过的方法。了解这些方法才能让我们的防御更坚固。5.1 常见绕过手法及应对大小写混淆/双写绕过攻击SeLeCt,UNIunionON。应对我们的代码已使用vbTextCompare和regEx.IgnoreCase True进行不区分大小写匹配。对于双写简单的替换一次会被绕过UNIunionON- 移除中间的union-UNION。我们的正则表达式全局匹配(GlobalTrue)会处理所有出现但双写是针对“替换删除”逻辑的。因此对于直接拦截的dangerPatterns我们用的是InStr查找只要包含即阻断双写无效。对于过滤的关键字我们替换为空双写可能会失效取决于正则引擎。更稳健的方法是在过滤后可以再次检查是否还有残留关键字。编码/十六进制绕过攻击将UNION SELECT编码为U%4eION %53ELECTURL编码或0x554e494f4e2053454c454354十六进制。在ASP中Request对象在获取值时会自动进行一次URL解码所以我们的过滤函数处理的是解码后的字符串简单的URL编码无效。但攻击者可能尝试双重编码或其他编码。应对确保我们的过滤发生在解码之后。对于十六进制数据库可能直接执行0x...这需要数据库层防御。我们的过滤器可以在dangerPatterns中加入对0x的检测。注释符与字符串拼接绕过攻击利用/**/代替空格如UNION/**/SELECT。或者利用数据库特性如‘‘OR‘‘1‘‘1。应对我们的正则表达式\s匹配空白字符/**/也能被匹配为空白符的一部分吗不一定。需要在dangerPatterns中加入/*和*/进行拦截。对于字符串拼接符在Access和SQL Server中常用也应加入过滤数组。宽字节注入针对GBK等编码攻击在GBK编码中‘的编码是%27。如果程序在转义时先URL解码得到‘然后转义为‘‘但数据库连接层或过滤逻辑存在编码问题攻击者输入%df%27程序可能将%df%27视为一个宽字符从而“吃掉”转义符\如果存在的话。ASP本身处理这个问题的场景较少但了解有益。应对统一网站、数据库连接字符串的字符集为UTF-8。在过滤前明确指定字符串的编码处理。5.2 超越过滤深度防御策略过滤是重要的一层但不应是唯一的一层。真正的安全需要纵深防御数据库最小权限原则连接数据库的应用程序账号只赋予其完成业务所需的最小权限。例如一个只需要查询的页面就用只有SELECT权限的账号连接。绝对不要使用sa或root等超级管理员账号。错误信息处理将ASP的默认错误页面关闭或定制统一的、友好的错误页面。切勿将数据库的原始错误信息如表名、列名、SQL语句直接显示给用户这会给攻击者提供宝贵的信息。On Error Resume Next ‘ 开启错误处理 ‘ … 执行数据库操作 … If Err.Number 0 Then ‘ 记录错误详情到服务器日志Err.Description, Err.Source Response.Write “系统繁忙请稍后再试。” ‘ 给用户模糊信息 Response.End End If输入类型强校验对于ID、年龄、页码等明确应为数字的参数在过滤前后都用IsNumeric()函数判断并强制转换为数值类型CLng()。Dim id id SafeRequest(“id”, “Get”) If Not IsNumeric(id) Then Response.Write “参数无效” Response.End End If id CLng(id) ‘ 转换为长整型 sql “SELECT * FROM products WHERE id “ id ‘ 注意这里没有单引号这里有个关键点如果参数是数字在拼接SQL时不应该加单引号。WHERE id ‘123‘和WHERE id 123都是正确的但前者是字符串比较后者是数字比较。如果加了单引号我们的过滤转义反而可能引入问题。所以类型校验和SQL拼接方式要匹配。6. 实战测试与效果验证部署完过滤代码后必须进行测试。不要假设它有效。6.1 手工测试用例你可以构造以下URL或表单数据进行测试基础注入?id1‘ AND ‘1‘‘1- 应被转义为1‘‘ AND ‘‘1‘‘‘‘1查询失败或返回正常结果非全部数据。关键字测试?searchunion select username, password from users-union和select应被过滤掉最终查询词可能变为username, password from users这可能导致SQL语法错误从而被我们的错误处理机制捕获返回统一错误页面。危险过程测试?cmd‘; EXEC xp_cmdshell ‘dir‘ --- 检测到;和xp_cmdshell应被直接拦截返回空值或跳转错误页。编码测试?name%4f‘R‘1‘%271(URL编码后的O‘R‘1‘‘1) - 经过ASP自动解码后我们的过滤器会处理单引号。正常业务测试输入包含合法单引号的内容如O‘Brien。过滤后应变为O‘‘Brien存入数据库后再查询出来显示应该还是O‘Brien因为数据库会正确解析两个单引号为一个。这是功能正确性的关键测试。6.2 使用工具辅助测试谨慎可以在本地或测试环境使用如SQLMap、Havij等自动化注入工具进行扫描。观察工具的Payload是否被我们的过滤器成功拦截。切记只能在你自己拥有完全权限的测试环境进行切勿对他人网站进行未授权的测试这是违法行为。部署过滤器后再用SQLMap扫描你应该会看到大量Payload被识别为“不是注入点”或请求被阻断。这可以从一个侧面验证效果。6.3 日志分析完善攻击日志记录功能至关重要。当过滤器触发拦截规则时应记录以下信息攻击时间攻击者IP (Request.ServerVariables(“REMOTE_ADDR”))攻击类型如SQLi_Keyword, SQLi_XSS攻击参数原始值攻击URL定期分析这些日志可以帮助你发现潜在的、试图绕过过滤的新型攻击模式从而及时更新你的过滤规则。7. 维护与迭代让防御代码“活”起来安全不是一劳永逸的。这段“最新”的代码随着时间推移也会变旧。关注漏洞情报订阅一些安全论坛、博客关注最新的SQL注入绕过技巧。当出现新的、广泛利用的Payload时评估其是否会影响你的系统并及时更新dangerPatterns数组和正则表达式规则。定期代码审计即使有了全局过滤也应定期检查代码中是否还存在直接拼接SQL的“漏网之鱼”。寻找所有 Request(“xxx”) 这样的模式。考虑升级或重构如果条件允许将核心业务从ASP迁移到更现代的、支持原生参数化查询如ASP.NET的SqlParameter PHP的PDO的平台是从根本上解决SQL注入的最佳途径。在重构之前本文提供的过滤方案是一个坚实可靠的临时防线。最后我想分享一个深刻的体会在安全领域“黑名单”过滤永远在被动追赶“白名单”验证。本文提供的是一套强化的黑名单方案它能在不修改大量业务代码的情况下极大提升ASP应用的安全性。但最理想的状态是在每一个数据输入点都根据其预期的格式数字、邮箱、特定字符集进行严格的白名单验证。在维护老旧系统时我们常常需要在理想和现实之间做出权衡。这套代码就是我找到的那个在实战中有效的平衡点。希望它能为你守护的系统筑起一道坚固的围墙。