原文  http://book.51cto.com/art/200710/58875.htm

存储过程sp_pivot的实现包含糟糕的编程习惯和安全隐患。就像我在本章的前面提到的,微软强烈建议不要在用户定义存储过程的名称中使用sp_前缀。一方面,把存储过程创建为特殊存储过程的会带来灵活性;但另一方面,你所依赖的行为得不到任何支持。所以最好放弃这种通过创建以sp_为前缀的存储过程获取的灵活性,在用户数据库中使用其他前缀创建用户定义存储过程。
代码定义的所有输入参数都未限制大小(使用MAX说明符),而且未作任何输入验证。因为存储过程调用的动态执行基于用户输入的字符串,限制输入的大小并检查潜在的SQL注入危险是非常重要的。对于现有的实现,黑客可以很容易地注入代码并破坏你的系统。你可以在第4章和联机丛书(http://msdn2.microsoft.com/en-us/library/ms161953 (SQL.90).aspx)中找到关于SQL注入的讨论。作为一个利用用户输入注入恶意代码的示例,观察下面这个对存储过程的调用。

EXEC Northwind.dbo.sp_pivot
@query    = N'dbo.Orders',
@on_rows  = N'1 AS dummy_col) DummyTable;
PRINT ''So easy to inject code here!
This could have been a DROP TABLE or xp_cmdshell command!'';
SELECT * FROM (select EmployeeID AS empid',
@on_cols  = N'MONTH(OrderDate)',
@agg_func = N'COUNT',
@agg_col  = N'*';

存储过程生成的查询字符串应该是这样的:

SELECT *
FROM
( SELECT
1 AS dummy_col) DummyTable;
PRINT 'So easy to inject code here!
This could have been a DROP TABLE or xp_cmdshell command!';
SELECT * FROM (select EmployeeID AS empid,
MONTH(OrderDate) AS pivot_col,
1 AS agg_col
FROM
( SELECT * FROM dbo.Orders
) AS Query
) AS PivotInput
PIVOT
( COUNT(agg_col)
FOR pivot_col
IN([1],[2],[3],[4],[5],[6],[7],[8],[9],[10],[11],[12])
) AS PivotOutput;

当执行这些代码时,注入的PRINT语句可以顺利执行。为了证明可以轻易地注入代码,我使用了一个无害的PRINT语句,但是很明显,这些恶意代码可以是任何有效的T-SQL代码。例如DROP TABLE语句、调用xp_cmdshell等。总之,在这些地方采取措施防范SQL注入是极其重要的。
该存储过程不仅未防范SQL注入,而且根本就没有执行任何输入验证。例如,应该验证输入的对象和列名称的有效性。该存储过程也没有包含错误处理。我会在第10章讨论错误处理,所以没有在修改后的解决方案中演示这一点。下面我将演示输入验证。

在呈现修改后的解决方案之前,先删除已经存在的sp_pivot:

USE master;
GO
IF OBJECT_ID('dbo.sp_pivot') IS NOT NULL
DROP PROC dbo.sp_pivot;

代码清单7-9是该任务的修改后的解决方案
代码清单7-9  创建usp_pivot存储过程的脚本

USE Northwind;
GO

IF OBJECT_ID('dbo.usp_pivot') IS NOT NULL
DROP PROC dbo.usp_pivot;
GO

CREATE PROC dbo.usp_pivot
@schema_name AS sysname      = N'dbo',
-- 表/视图的架构
@object_name AS sysname      = NULL, 
-- 表/视图的名称
@on_rows     AS sysname      = NULL, -- 分组列
@on_cols     AS sysname      = NULL, -- 旋转列
@agg_func    AS NVARCHAR(12) = N'MAX',
-- 聚集函数
@agg_col     AS sysname      = NULL -- 统计列
AS

DECLARE
@object  AS NVARCHAR(600),
@sql     AS NVARCHAR(MAX),
@cols    AS NVARCHAR(MAX),
@newline AS NVARCHAR(2),
@msg     AS NVARCHAR(500);

SET @newline = NCHAR(13) + NCHAR(10);
SET @object  = QUOTENAME(@schema_name) + N'.' + QUOTENAME(@object_name);

-- 检查是否缺少输入
IF   @schema_name IS NULL
OR @object_name IS NULL
OR @on_rows     IS NULL
OR @on_cols     IS NULL
OR @agg_func    IS NULL
OR @agg_col     IS NULL
BEGIN
SET @msg = N'Missing input parameters: '
+ CASE WHEN @schema_name IS NULL 
THEN N'@schema_name;' ELSE N'' END
+ CASE WHEN @object_name IS NULL 
THEN N'@object_name;' ELSE N'' END
+ CASE WHEN @on_rows     IS NULL 
THEN N'@on_rows;'     ELSE N'' END
+ CASE WHEN @on_cols     IS NULL 
THEN N'@on_cols;'     ELSE N'' END
+ CASE WHEN @agg_func    IS NULL 
THEN N'@agg_func;'    ELSE N'' END
+ CASE WHEN @agg_col     IS NULL 
THEN N'@agg_col;'     ELSE N'' END
RAISERROR(@msg, 16, 1);
RETURN;
END 
-- 只允许已存在的表或视图作为输入对象
IF COALESCE(OBJECT_ID(@object, N'U'),
OBJECT_ID(@object, N'V')) IS NULL
BEGIN
SET @msg = N'%s is not an existing table or view in the database.';
RAISERROR(@msg, 16, 1, @object);
RETURN;
END

-- 检查 @on_rows, @on_cols, @agg_col 中的列名称是否存在

IF   COLUMNPROPERTY(OBJECT_ID(@object), @on_rows, 'ColumnId') IS NULL
OR COLUMNPROPERTY(OBJECT_ID(@object), @on_cols, 'ColumnId') IS NULL
OR COLUMNPROPERTY(OBJECT_ID(@object), @agg_col, 'ColumnId') IS NULL
BEGIN
SET @msg = N'%s, %s and %s must'
+ N' be existing column names in %s.';
RAISERROR(@msg, 16, 1, @on_rows, @on_cols, @agg_col, @object);
RETURN;
END

-- 检查@agg_func是否是已知的函数
-- 根据需要增加该清单并相应调整@agg_func的大小 
IF @agg_func NOT IN
(N'AVG', N'COUNT', N'COUNT_BIG', N'SUM', N'MIN', N'MAX',
N'STDEV', N'STDEVP', N'VAR', N'VARP')
BEGIN
SET @msg = N'%s is an unsupported aggregate function.';
RAISERROR(@msg, 16, 1, @agg_func);
RETURN;
END

-- 构造列列表
SET @sql =
N'SET @result = '                                    + @newline +
N'  STUFF('                                          + @newline +
N'    (SELECT N'','' + '
+ N'QUOTENAME(pivot_col) AS [text()]'       + @newline +
N'     FROM (SELECT DISTINCT('
+ QUOTENAME(@on_cols) + N') AS pivot_col'   + @newline +
N'           FROM ' + @object + N') AS DistinctCols' + @newline +
N'     ORDER BY pivot_col'                           + @newline +
N'     FOR XML PATH('''')),'                         + @newline +
N'    1, 1, N'''');'

EXEC sp_executesql
@stmt   = @sql,
@params = N'@result AS NVARCHAR(MAX) OUTPUT',
@result = @cols OUTPUT;

-- 检查 @cols 是否存在SQL 注入尝试
IF   UPPER(@cols) LIKE UPPER(N'%0x%')
OR UPPER(@cols) LIKE UPPER(N'%;%')
OR UPPER(@cols) LIKE UPPER(N'%''%')
OR UPPER(@cols) LIKE UPPER(N'%--%')
OR UPPER(@cols) LIKE UPPER(N'%/*%*/%') 
OR UPPER(@cols) LIKE UPPER(N'%EXEC%')
OR UPPER(@cols) LIKE UPPER(N'%xp[_]%')
OR UPPER(@cols) LIKE UPPER(N'%sp[_]%')
OR UPPER(@cols) LIKE UPPER(N'%SELECT%')
OR UPPER(@cols) LIKE UPPER(N'%INSERT%')
OR UPPER(@cols) LIKE UPPER(N'%UPDATE%')
OR UPPER(@cols) LIKE UPPER(N'%DELETE%')
OR UPPER(@cols) LIKE UPPER(N'%TRUNCATE%')
OR UPPER(@cols) LIKE UPPER(N'%CREATE%')
OR UPPER(@cols) LIKE UPPER(N'%ALTER%')
OR UPPER(@cols) LIKE UPPER(N'%DROP%')
-- 其他一些可能用于SQL注入的字符串
BEGIN
SET @msg = N'Possible SQL injection attempt.';
RAISERROR(@msg, 16, 1);
RETURN;
END

-- 创建PIVOT查询
SET @sql =
N'SELECT *'                                          + @newline +
N'FROM'                                              + @newline +
N'  ( SELECT '                                       + @newline +
N'      ' + QUOTENAME(@on_rows) + N','               + @newline +
N'      ' + QUOTENAME(@on_cols) + N' AS pivot_col,'  + @newline +
N'      ' + QUOTENAME(@agg_col) + N' AS agg_col'     + @newline +
N'    FROM ' + @object                               + @newline +
N'  ) AS PivotInput'                                 + @newline +
N'  PIVOT'                                           + @newline +
N'    ( ' + @agg_func + N'(agg_col)'                 + @newline +
N'      FOR pivot_col'                               + @newline +
N'        IN(' + @cols + N')'                        + @newline +
N'    ) AS PivotOutput;';

EXEC sp_executesql @sql;

该存储过程的实现遵循了良好的编程习惯并解决了前面提到的安全缺陷。但是要记住,当根据用户输入和存储数据/元数据构造代码时,要完全地防范SQL注入是非常困难的。

存储过程usp_pivot是在Northwind数据库中以usp_前缀创建的用户定义存储过程。这意味着它只能与Northwind中的表和视图进行交互,从这个意义来讲,它不如前面的实现那样灵活。但是你可以在Northwind中创建用于查询其他数据库对象的视图,并把该视图作为输入提供给这个存储过程。

usp_pivot存储过程的代码提供了几种防范SQL注入尝试的措施:

 限制输入参数的大小。
 存储过程只接收在数据库中存在的有效的表或视图名称,不接收其他形式的查询。同样,存储过程中的输入参数@on_rows、@on_cols和@agg_co只接收在输入表/视图中存在的有效的列名称,不能是任意的T-SQL表达式。你可以使用任意的查询创建视图,然后把它作为该存储过程的输入。
 代码在引用对象和列名称的地方使用了QUOTENAME,并用方括号作为分隔标识符。
 存储过程的代码检查@cols变量是否存在注入的代码字符串,它们可能通过存储被串联起来的旋转列值注入。
代码还对输入执行检查以确保提供了所有参数,这些表/视图和列名称存在,输入的聚集函数包含在支持的函数列表中。关于错误处理,我将在第10章再讨论。

usp_pivot存储过程看起来没有sp_pivot灵活,但你可以创建视图为usp_pivot提供数据。例如,考虑下面的代码,在前面曾用它返回按订单年份旋转的每个员工的订单金额合计(数量*单价):

EXEC Northwind.dbo.sp_pivot
@query    = N'
SELECT O.OrderID, EmployeeID, OrderDate, Quantity, UnitPrice
FROM dbo.Orders AS O
JOIN dbo.[Order Details] AS OD
ON OD.OrderID = O.OrderID',
@on_rows  = N'EmployeeID AS empid',
@on_cols  = N'YEAR(OrderDate)',
@agg_func = N'SUM',
@agg_col  = N'Quantity*UnitPrice';

通过创建一个包含所需数据的视图,你就可以利用usp_pivot实现同样的功能。

USE Northwind;
GO
IF OBJECT_ID('dbo.ViewForPivot') IS NOT NULL
DROP VIEW dbo.ViewForPivot;
GO

CREATE VIEW dbo.ViewForPivot
AS

SELECT
O.OrderID       AS orderid,
EmployeeID      AS empid,
YEAR(OrderDate) AS order_year,
Quantity * UnitPrice AS val
FROM dbo.Orders AS O
JOIN dbo.[Order Details] AS OD
ON OD.OrderID = O.OrderID;
GO

然后调用usp_pivot,就像这样:

EXEC dbo.usp_pivot
@object_name = N'ViewForPivot',
@on_rows  = N'empid',
@on_cols  = N'order_year',
@agg_func = N'SUM',
@agg_col  = N'val';

你将得到前面表7-13所示的输出。
相对于你的系统安全而言,这只是很小的代价。
完成后,运行下面的代码进行清理。

USE Northwind;
GO
IF OBJECT_ID('dbo.ViewForPivot') IS NOT NULL
DROP VIEW dbo.ViewForPivot;
GO
IF OBJECT_ID('dbo.usp_pivot') IS NOT NULL
DROP PROC dbo.usp_pivot;

动态Pivot(2)的更多相关文章

  1. 动态Pivot(1)

    原文 http://book.51cto.com/art/200710/58874.htm 7.7  动态Pivot 作为另外一个练习,假设你要编写一个存储过程,它生成动态Pivot查询.这个存储过程 ...

  2. 动态PIVOT行转列

    id name subject score remark1 l math 86 2 l eng 68 3 l phy 88 4 z chn 99 5 z math 92 6 z com 98 7 z ...

  3. SQL 动态PIVOT查询

    DECLARE @sql_str VARCHAR(8000)DECLARE @sql_col VARCHAR(8000) SELECT @sql_col = ISNULL(@sql_col + ',' ...

  4. Pivot 和 Unpivot

    在TSQL中,使用Pivot和Unpivot运算符将一个关系表转换成另外一个关系表,两个命令实现的操作是“相反”的,但是,pivot之后,不能通过unpivot将数据还原.这两个运算符的操作数比较复杂 ...

  5. SQL Server 动态行转列(参数化表名、分组列、行转列字段、字段值)

    一.本文所涉及的内容(Contents) 本文所涉及的内容(Contents) 背景(Contexts) 实现代码(SQL Codes) 方法一:使用拼接SQL,静态列字段: 方法二:使用拼接SQL, ...

  6. SQL Server 动态行转列(轉載)

    一.本文所涉及的内容(Contents) 本文所涉及的内容(Contents) 背景(Contexts) 实现代码(SQL Codes) 方法一:使用拼接SQL,静态列字段; 方法二:使用拼接SQL, ...

  7. SQL server 2005 PIVOT运算符的使用

    原文:SQL server 2005 PIVOT运算符的使用 PIVOT,UNPIVOT运算符是SQL server 2005支持的新功能之一,主要用来实现行到列的转换.本文主要介绍PIVOT运算符的 ...

  8. Sqlserver中PIVOT行转列透视操作

    创建表: IF OBJECT_ID('T040_PRODUCT_SALES') IS NOT NULL DROP TABLE T040_PRODUCT_SALES create table T040_ ...

  9. SQL Server中动态列转行

    http://www.cnblogs.com/gaizai/p/3753296.html 一.本文所涉及的内容(Contents) 本文所涉及的内容(Contents) 背景(Contexts) 实现 ...

随机推荐

  1. 一、Linux启动过程详解

    启动第一步--加载BIOS当你打开计算机电源,计算机会首先加载BIOS信息,BIOS信息是如此的重要,以至于计算机必须在最开始就找到它.这是因为BIOS中包含了CPU的相关信息.设备启动顺序信息.硬盘 ...

  2. poj 2515 Birthday Cake

    /** 大意 : 求1^m + 2^m + 3^m + 4^m +....+ n^m 解题步骤: 先构造从0到m的第1阶差分序列,然后以下所有2---->p阶的差分表. 令C[n+1][1]=n ...

  3. 转: requirejs中文api (详细)

    RequireJS的目标是鼓励代码的模块化,它使用了不同于传统<script>标签的脚本加载步骤.可以用它来加速.优化代码,但其主要目的还是为了代码的模块化.它鼓励在使用脚本时以modul ...

  4. python 安装ssh和Scrapy

    在Python中没有专用的SSH模块,这需要手动的安装模块才行.Python中使用SSH需要用到OpenSSH,而OpenSSH依赖于paramiko模块,paramiko模块又依赖于pycrypto ...

  5. Linux和windows下的shutdown命令

    Linux下的shutdown命令 shutdown [options] [time] [wall] options: --help 获取帮助 -h 关机 -r 重启 -c 取消 -k 仅显示警告信息 ...

  6. MFC自绘控件学习总结第二贴---转

    首先感谢大家对第一帖的支持,应一些网友烈要求下面我在关于上一贴的一些补充和说明(老鸟可以无视)这一贴是实战+理论不知道第一帖的先看第一帖:http://topic.csdn.net/u/2011071 ...

  7. cocos2d-html5 简易 下拉表单 控件

    刚才在CH5的群里问了问  有没有大侠写过 下拉表单控件啊!  没人鸟窝 ,DZ老师表示非常伤心啊  ,于是乎  自己写一个把 共享给大家. 效果图上一个  仅仅实现了一个最最主要的控件  非常eas ...

  8. bootstrap框架开发电子商城案例

    bootstrap框架开发电子商城案例 玛图 bootstrap 商城框架

  9. 从数据库中,绑定JQuery Accordion控件---Repeater control

    http://aspsnippets.com/Articles/jQuery-Accordion-example-in-ASPNet-using-C-and-VBNet.aspx 1. 添加JQuer ...

  10. POJ 1159 回文LCS滚动数组优化

    详细解题报告可以看这个PPT 这题如果是直接开int 5000 * 5000  的空间肯定会MLE,优化方法是采用滚动数组. 原LCS转移方程 : dp[i][j] = dp[i - 1][j] + ...