动态Pivot(2)

原文  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;
时间: 2025-01-26 05:43:24

动态Pivot(2)的相关文章

动态Pivot(1)

原文 http://book.51cto.com/art/200710/58874.htm 7.7  动态Pivot 作为另外一个练习,假设你要编写一个存储过程,它生成动态Pivot查询.这个存储过程接收这些参数(都是Unicode字符串类型):@query.@on_rows.@on_cols.@agg_func和 @agg_col.你要根据这些输入构造PIVOT查询串并动态执行.下面是对输入参数的描述:  @query 提供给PIVOT 运算符的查询或表/视图的名称. @on_rows 用

SQL Server中如何动态行转列

SQL Server 动态行转列(参数化表名.分组列.行转列字段.字段值) 一.本文所涉及的内容(Contents) 本文所涉及的内容(Contents) 背景(Contexts) 实现代码(SQL Codes) 方法一:使用拼接SQL,静态列字段: 方法二:使用拼接SQL,动态列字段: 方法三:使用PIVOT关系运算符,静态列字段: 方法四:使用PIVOT关系运算符,动态列字段: 扩展阅读一:参数化表名.分组列.行转列字段.字段值: 扩展阅读二:在前面的基础上加入条件过滤: 参考文献(Refe

SQL Server 动态行转列

  一.本文所涉及的内容(Contents) 本文所涉及的内容(Contents) 背景(Contexts) 实现代码(SQL Codes) 方法一:使用拼接SQL,静态列字段; 方法二:使用拼接SQL,动态列字段; 方法三:使用PIVOT关系运算符,静态列字段; 方法四:使用PIVOT关系运算符,动态列字段; 二.背景(Contexts) 其实行转列并不是一个什么新鲜的话题了,甚至已经被大家说到烂了,网上的很多例子多多少少都有些问题,所以我希望能让大家快速的看到执行的效果,所以在动态列的基础上

SQL server 2005 PIVOT运算符的使用

原文:SQL server 2005 PIVOT运算符的使用         PIVOT,UNPIVOT运算符是SQL server 2005支持的新功能之一,主要用来实现行到列的转换.本文主要介绍PIVOT运算符的操作,以及如何实现动态PIVOT的行列转换.        关于UNPIVOT及SQL server 2000下的行列转换请参照本人的其它文章. 一.PIVOT的语法 SELECT [non-pivoted column], -- optional [additional non-p

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

原文:SQL Server 动态行转列(参数化表名.分组列.行转列字段.字段值) 一.本文所涉及的内容(Contents) 本文所涉及的内容(Contents) 背景(Contexts) 实现代码(SQL Codes) 方法一:使用拼接SQL,静态列字段: 方法二:使用拼接SQL,动态列字段: 方法三:使用PIVOT关系运算符,静态列字段: 方法四:使用PIVOT关系运算符,动态列字段: 扩展阅读一:参数化表名.分组列.行转列字段.字段值: 扩展阅读二:在前面的基础上加入条件过滤: 参考文献(R

Android 下拉刷新框架实现、仿新浪微博、QQ好友动态滑到底部自动加载

苦苦找寻的2个版本,经过测试好用.再次感谢原作者! 1.第一个版本 Android 下拉刷新框架实现 http://blog.csdn.net/leehong2005/article/details/12567757 前段时间项目中用到了下拉刷新功能,之前在网上也找到过类似的demo,但这些demo的质量参差不齐,用户体验也不好,接口设计也不行.最张没办法,终于忍不了了,自己就写了一个下拉刷新的框架,这个框架是一个通用的框架,效果和设计感觉都还不错,现在分享给各位看官. 1. 关于下拉刷新 下拉

C#实现Excel动态生成PivotTable_C#教程

Excel 中的透视表对于数据分析来说,非常的方便,而且很多业务人员对于Excel的操作也是非常熟悉的,因此用Excel作为分析数据的界面,不失为一种很好的选择.那么如何用C#从数据库中抓取数据,并在Excel 动态生成PivotTable呢?下面结合实例来说明. 一般来说,数据库的设计都遵循规范化的原则,从而减少数据的冗余,但是对于数据分析来说,数据冗余能够提高数据加载的速度,因此为了演示透视表,这里现在数据库中建立一个视图,将需要分析的数据整合到一个视图中.如下图所示: 数据源准备好后,我们

SQL Server中行列转换 Pivot UnPivot

原文:SQL Server中行列转换 Pivot UnPivot PIVOT用于将列值旋转为列名(即行转列),在SQL Server 2000可以用聚合函数配合CASE语句实现 PIVOT的一般语法是:PIVOT(聚合函数(列) FOR 列 in (-) )AS P 完整语法: table_source PIVOT( 聚合函数(value_column) FOR pivot_column IN(<column_list>) )   UNPIVOT用于将列明转为列值(即列转行),在SQL Ser

oracle-Oracle动态行转列问题

问题描述 Oracle动态行转列问题 有什么好的动态行转列方法?(动态噢!) 看过pivot xml函数但结果是CLOB类型,Java读取没写出来, 请问decode函数能实现动态的行转列吗? 解决方案 http://www.cnblogs.com/QQParadise/articles/1712093.html 解决方案二: /*物料 需要数量 需要仓库 现存量仓库 现存量仓库数量 批次 A1 2 C1 C1 20 123 A1 2 C1 C2 30 111 ......答案就在这里:ORAC