原文  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. [LeetCode]题解(python):022-Generate Parentheses

    题目来源: https://leetcode.com/problems/generate-parentheses/ 题意分析: 题目输入一个整型n,输出n对小括号配对的所有可能性.比如说,如果输入3, ...

  2. Regex阅读笔记(一)之入门

    在检查一行文本时,^代表一行的开始,$代表结束. 字符数组:[],在里面列举任意多个字符,可以匹配其中任意一个字符,字符组元字符'-'表示一个范围. ^$表示一个空行(没有任何字符,包括空白字符) [ ...

  3. Application和Session的例子

    %@ Page Language="C#" AutoEventWireup="true" CodeBehind="ApplicationTest.as ...

  4. cocos2dx进阶学习之CCNode

    继承关系 CCNode  -> CCObject CCNode在cocos2dx中抽象舞台对象,需要渲染的对象都是从CCNode派生,包括CCScene,CCLayer,CCSprite等等 C ...

  5. 最长回文串(manacher算法)

    ; ; int p[N]; char str[LEN], tmp[N]; //p[i]表示以str[i]为中心的回文往右延伸的 最长长度 void manacher(char* str, int* p ...

  6. ceph源码之一

    转自于:http://blog.csdn.net/changtao381/article/details/8698935 一.概述: 其结构如下:在src 里, 网络通信:  msg  里面 包括了网 ...

  7. oracle误删的表恢复

    flashback table lip_organization to before drop;

  8. MYSQL - php 使用 localhost 无法连接数据库

    php 使用 localhost 无法连接数据库,而使用127.0.0.1却能连接成功. 可能原因: 系统hosts文件未提供127.0.0.1到localhost的解析.解决方法(以win7系统为例 ...

  9. A - Number Sequence(矩阵快速幂或者找周期)

    Description A number sequence is defined as follows: f(1) = 1, f(2) = 1, f(n) = (A * f(n - 1) + B * ...

  10. java类和对象的基础(笔记)

    在Java类的设计中,有时希望一个类在任何时候只能有一个实例.这时可以将该类设计为单例模式(singleton).要将一个类设计为单例模式,需要把类的构造方法的访问修饰符声明为private,然后在类 ...