Archive

Posts Tagged ‘ADO.NET’

การป้องกันการโจมตีด้วย SQL Injection , SQL Injection Attacks

December 29, 2008 2 comments

ในเรื่องที่จะพูดถึงนี้ น่าจะพูดได้ว่าเป็นเกร็ดเล็กเกร็ดน้อยที่ ผู้พัฒนาควรจะรู้และเป็นประโยชน์มากเมื่อนำไปประยุกต์ใช้งาน เรื่องที่จะพูดถึงก็คือ การป้องกันการโจมตีด้วย

SQL Injection มันเป็นอย่างไร พูดอย่างง่าย ๆ ก็คือ กระบวนการหรือการ ส่งหรือใส่ SQL code ไปยัง application โดยที่ผู้พัฒนาไม่ได้ตั้งใจหรือไม่ได้เจตนาที่ทำให้เกิด SQL code แบบนั้น ( เกิดจากบุคคลที่ 3 ด้วยความปรารถนาที่เป็นลบนั่นเอง ) คงพอจะเข้าใจนะครับ  ซึ่งเหตุการณ์แบบนี้จะเกิดขึ้นได้ก็ต่อเมื่อ application นั้นถูกออกแบบมาอย่างไม่ดีเท่าที่ควร หรือที่เรียกว่า poor design นะครับ  — ส่วนใหญ่มันมักถูกจะออกแบบมาแบบนั้นซะด้วยซิ ว่าไหมครับ  — ซึ่งมันก็จะเกิดกับ application ที่ใช้เทคนิค การสร้าง SQL String ให้กับ Command Object โดยการรับข้อมูลมาจาก ผู้ใช้งาน คงจะนึกภาพไม่ออก มาดูตัวอย่างเพื่อให้มองเห็นภาพอย่างชัดเจนดีกว่านะครับ

String sql =
“SELECT Orders.CustomerID, Orders.OrderID, COUNT(UnitPrice) AS Items, “ +
“SUM(UnitPrice * Quantity) AS Total FROM Order “ +
“INNER JOIN OrderDetails “ +
“ON Orders.OrderID = OrderDetail.OrderID “ +
“WHERE Orders.CustomerID = ‘” + txtID.Text +” ’ ” +
“GROUP BY Orders.OrderID, Orders.CustomerID”;

SqlCommand cmd = new SqlCommand(sql,connection);

จากตัวอย่าง ผู้บุกรุกอาจจะทดสอบ SQL statement ด้วยวิธีการต่าง ๆ โดยจุดประสงค์แรกก็คือเพื่อให้ได้มาซึ่ง error Message ซึ้งถ้า Error นั้นไม่ถูกปล่อยปะละเลยหรือไม่ถูกดำเนินอย่างเหมาะสมแล้วละก็ System Error message อาจถูกแสดงออกไป และเป็นประโยชน์ต่อผู้บุกรุกที่จะนำไปใช้ต่อไปได้อีก
ตัวอย่าง ลองคิดกันดูนะครับว่าจะเกิดอะไรขึ้นถ้า user ป้อนข้อมูลนี้ลงใน TextBox จากตัวอย่างข้างต้น

ALFKI’ OR ‘1’=’1

จะส่งผลให้ SQL Statement เป็นดังนี้

SELECT Orders.CustomerID, Orders.OrderID, COUNT(UnitPrice) AS Items,
SUM(UnitPrice * Quantity) AS Total FROM Orders
INNER JOIN [Order Details]
ON Orders.OrderID = [Order Details].OrderID
WHERE Orders.CustomerID = ‘ALFKI’ OR ‘1’=’1′
GROUP BY Orders.OrderID, Orders.CustomerID

พิจารณา Expression หลัง WHERE จะเห็นว่า ข้อมูลทั้งหมดในตาราง Orders จะถูกแสดงออกมา ใช่ไหมครับ ผมคงไม่ต้องอธิิบายรายละเอียดนะครับ เนื่องจากว่า มันเป็น Logic ของ OR ใช่ไหมครับ ถ้าค่าด้านหนึ่งด้านใด เป็น จริง ก็จะส่งผลให้ทั้ง Statement เป็น จริงแมนบ่ ครับ  น่านแหละครับ 1=1 เป็น จริง ไม่ว่า CustomerID จะเป็นอะไรก็ตาม ทั้งหมดเป็นจริง

sqlinject11ครับ คิดต่ออีกนิดถ้าหากว่าข้อมูลดังกล่าวนั้นเป็นข้อมูลที่ Sensitive หละครับ เช่น Social Security numbers  วันเกิด   หรือ  ข้อมูล Credit card   ละก็จะถือว่านี้เป็นปัญหาใหญ่หลวง ซึ่งอาจก่อให้เกิดความเสียหายมหาศาล ใช่ใหม่ครับ

ตัวอย่างวิธีการที่ซับซ้อนมากกว่านั้นขึ้นไปอีกเช่น
ผู้บุกรุกสามารถ comment ส่วนท้ายของ SQL Statement โดยใช้ — สำหรับ SQL Server ( MySql ใช้ # และ Oracle ใช้ ; ) หรือ ใช้ การสั่งงานแบบ Batch ได้เพื่อเพิ่ม SQL Code เข้าไปทำงานอย่างใดอย่างหนึ่ง ในกรณีนี้ สำหรับ SQL Server ก็แค่เพียงเพิ่ม semicolon และตามด้วย Command ที่ีต้องการ ซึ่งทำให้ผู้บุกรุกสามารถ ลบข้อมูลใน Table อื่นได้ หรือ สามารถเรียกใช้ SQL Server xp_cmdshell system stored procedure เพื่อประมวลผลโปรแกรม ที่ command line ได้ เป็นไงหล่ะครับ ไปได้ถึงขนาดนั้น
ครับลองดูตัวอย่างนี้นะครับ ถ้าผู้ใช้งานกรอกข้อมูลนี้ลงใน txtID TextBox ลองพิจารณาดูนะครับ

ALFKI’ ; DELETE * FROM Customers —

หมายความว่า หลังจากที่ได้แสดงข้อมูล Orders ของ CustomerID เท่ากับ ALFKI แล้ว ต่อด้วยการลบข้อมูลทั้งหมดในตาราง Customers และยกเลิก คำสั่งต่อท้ายที่เหลือทั้งหมด ครับ

ว่ากันด้วยการป้องกัน —
ต่อมา เราลองมาดูกันว่าเราจะทำอย่างไรถึงจะสามารถจัดการหรือป้องกันการโจมตีแบบนี้ ได้บ้าง
แน่นอนครับ สำหรับโปรแกรมเมอร์แล้วเราสามารถการปฏิบัติตาม แนวทางที่เราเชื่อว่าเป็นแนวทางการปฏิบัติที่ดีเช่น การจำกัดความยาวของข้อมูลที่อนุญาตให้ป้อนลงใน TextBox โดยการกำหนด TextBox.MaxLength ไม่ให้ยาวเกินความจำเป็น ซึ่งเป็นการลดโอกาสที่ ผู้รุกจำสามารถป้อน script ยาว ๆ ลงไปได้ ยิ่งไปกว่านั้น เราต้องทำการจำกัดข้อมูลของ Error message ถ้าเราดัก exception ของ database เราก็ควรที่จะแสดงเฉพาะข้อความที่เป็นคำอธิบาย เช่น “Data source error” แทนที่จะแสดง ข้อมูลใน Exception.Message ซึ่งอาจจะเป็นจะเป็นการชี้ โพรงให้กระรอกได้ ( ช่องโหว่ของระบบได้ )

และยิ่งไปกว่านั้น คุณก็ต้อง จัดการกับ character พิเศษต่าง เช่น แปลง single quotation marks (‘) เป็น two quotation marks (“) เพื่อให้ไม่ทำให้เกิดความสับสนกับ ตัวปิดหัวท้าย

string ID = txtID.Text().Replace(“‘”,”””);

ครับ ไม่ได้หมายถึงมันจะจบสิ้นนะครับ มันจะนำมาซึ่ง ความปวดหัวเพิ่มเข้าไปอีกถ้า ค่าของ txtID.Text นั้นมี apostrophes ขึ้นมา ซึ่งมันอาจจะเกิดขึ้นได้ อีกต่อ ๆ ไปซึ่งสรุปได้ว่า แนวทางปฏิบัติดังกล่าวไม่สามารถแก้ไขปัญหา SQL Injection ได้อย่างสมบูรณ์แบบ ผมสรุปแบบนี้ไม่ใช่ว่ามันจะจบลงแบบนี้หรอกนะครับ เพียงแต่จะบอกว่ามีวิธีการที่ดีกว่านั้นสำหรับการป้องกัน SQL injection attack ซึ่งผมจะไ้ด้กล่าวต่อไป

การป้องกัน SQL injection attack โดยการ ใช้ Parameterized Commands

Parameterized Command ใช้ placeholders ใน SQL text ซึ่ง Parameterized command นี้เป็นตัวกำหนด dynamic values ที่จะส่งผ่านมาทาง Parameters collection ของ Command object ดูดังตัวอย่าง นะครับ
จาก SQL Statement
SELECT * FROM Customers WHERE CustomerID = ‘ALFKI’
เปลี่ยนเป็น
SELECT * FROM Customers WHERE CustomerID = @CustID

ลองมาดูตัวอย่างเต็ม ๆ กันนะครับ ( สมมติว่าเราสร้าง connection object ไว้แล้ว )

String sql =
“ SELECT Orders.CustomerID, Orders.OrderID, COUNT(UnitPrice) AS Items, “ +
“ SUM(UnitPrice * Quantity) AS Total FROM Orders “ +
“ INNER JOIN OrderDetails “ +
“ ON Order.OrderID = OrderDetails.OrderID “ +
“ WHERE Orders.CustomerID = @CustID “ +
“ GROUP BY Orders.OrderID, Orders.CustomerID” ;
SqlCommand cmd = new SqlCommand(sql,con);
cmd.Parameters.Add(“@CustID”, txtID.Text);

ถ้าเราลองทำ SQL injection attack กับ code ที่ถูกปรับปรุงใหม่แล้ว เราจะบบว่าโปรแกรมจะไม่ส่งข้อมูลใดออกมาแสดงผลเลยเนื่องจาก นั่นก็หมายความว่าไม่มี order items ใดที่มีข้อมูล customerID มีค่าเท่ากับ ALFKI’ OR ‘1’=’1 ซึ่งเป็นสิ่งที่คุณต้องการใช่ไหมครับ

การป้องกัน SQL injection attack โดยการ ใช้ Stored Procedures
Stored Procedures ผมคงจะไม่อธิบายว่ามันคืออะไรทำงานอย่างไรหรอกนะครับ คิดว่าท่านทั้งหลาย รู้จักดีอยู่แล้ว แต่จะขอกล่าวถึงประโยชน์ของมันสักนิดหนึ่ง คือ

  • ง่ายต่อการบำรุงรักษา หมายถึงว่าตัว stored procedures นั้นมันแยกอยู่ต่างหากกับโปรแกรมที่เรียกใช้มัน เพราะฉะนั้นหากจะต้องมีการเปลี่ยนแปลงอะไรที่ stored procedure ก็สามารถทำได้โดยไม่ต้องมีการ Recompile ทั้งโปรแกรมนั่นเอง
  • ทำใ้ห้เราสามารถใช้งาน database ในวิถีทางที่ปลอดภัยได้ เราสามารถที่จะกำหนดให้ stored procedures สามารถเข้าถึงเฉพาะ tables ที่กำหนดได้
  • – ทำให้ประสิทธิภาพการทำงานดีขึ้น เนื่องจาก stored procedures สามารถรวม หลาย ๆ คำสั่งทำงานในคราวเดียวกันได้ ซึ่งทำให้เราสามารถทำงาน ให้เสร็จหลาย ๆงานพร้อมกันได้ต่อหนึ่งรอบการเข้าถึง database

มาดูตัวอย่างกันดีกว่านะครับว่าจะใช้งานอย่างไร ซึ่งจริงแล้วไม่ก็คือ การใช้ Place holder กับ Parameterized Command Object นั่นแหละ ครับ เพียงแต่เราเรียกใช้ stored procedure แทนที่จะใช้การส่งผ่าน sql text สมมติว่าเราสร้าง Stored procedure ไว้ดังนี้นะครับ

CREATE PROCEDURE InsertEmployee
@TitleOfCourtesy varchar(25),
@LastName varchar(20),
@FiratName varchar(10),
@EmployeeID int OUTPUT
AS
INSERT INTO Employees ( TitleOfCourtesy, LastName, FirstName, HireDate )
VALUES (@TitleOfCourtesy, @LastName, @FirstName, GETDATE());

SET @EmployeeID = @@IDENTITY
GO

Stored procedures มี parameters 3 ตัวคือ TitleOfCourtesy, LastName, และ FirstName ซึ่ง return ID ของ record ออกมาทาง output parameters ชื่อว่า @EmployeeID

ต่อไปเราสร้าง SqlCommand เพื่อเรียกไปยัง stored procedure command นี้มี input Parameters 3 ตัวเช่นเดียวกัน และ ส่งค่า ID ของ record ใหม่ออกมาให้ต่อไปเป็นตัวอย่างของการสร้าง

SqlCommand cmd = new SqlCommand(“InsertEmployee”, con);
cmd.CommandType = CommandType.StoredProcedure;

ต่อไปเราก็เพิ่ม parameters ของ stored procedures เข้าไปที่ Collection ของ Command.Parameters ซึ่งเราต้องกำหนด data type และ ขนาดของ parameter นั้นด้วย ดังนี้

cmd.parameters.Add(new SqlParameter(“@TitleOfCourtesy”, SqlDbType.NVarChar,25));
cmd.Parameters[“@TitleOfCourtesy”].Value = title;
cmd.Parameters.Add(new SqlParameter(“@LastName”, SqlDbType.NVarChar, 20));
cmd.Parameters[“@LastName”].Value = lastName;
cmd.Parameters.Add(new SqlParameter(“@FirstName”, SqlDbType.NVarChar, 10));
cmd.Parameters[“@FirstName”].Value = firstName;

cmd.Parameters.Add(new SqlParameter(“@EmployeeID”, SqlDbType.Int, 4));
cmd.Parameters[“@EmployeeID”].Direction = ParameterDirection.Output;

สังเกตว่า Parameter ตัวสุดท้ายนั้นเป็น output parameter นะครับ

สุดท้ายเราก็ทำการ run

con.Open();
try
{
int numAff = cmd.ExecuteNonQuery();
HtmlContent.Text += String.Format(“Inserted {0} record(s)
“, numAff);
// Get the newly generated ID.
empID = (int)cmd.Parameters[“@EmployeeID”].Value;
HtmlContent.Text += “New ID: ” + empID.ToString();
}
finally
{
con.Close();
}

ครับทั้งหมด ก็มาจบตรงที่ สองวิธีการสุดท้ายที่เราสามารถที่จะป้องกันการ โจมตี ด้วย SQL Injection ได้อย่างมีประสิทธิภาพ ก็คือการใช้ Parameterized Commands และการใช้ Stored procedures นั่นเอง นี่เป็น ส่วนหนึ่งของการแก้ปัญหานี้นะครับ
หากต้องการข้อมูลเพิ่มเติมละก็ ผมใช้ข้อมูลจากหนังสือเล่มนี้นะครับ Pro ASP.NET 2.0 in C# 2005

การทำ Master-Detail โดยใช้ BindingSource,Master-Detail using BindingSource

December 23, 2008 2 comments

ในการสร้าง Database Application นั่นในบางครั้งเราอาจจะต้องการ การแสดงผลในลักษณะที่เป็น Master-Detail คือมีตารางหลักและตารางที่แสดงผล Detail อีกหนึ่งตาราง เมื่อคลิกที่ record ใด record หนึ่งในตารางหลัก ก็จะนำไปสู่การแสดงผลที่ตารง Detail ที่สัมพันธ์กับข้อมูลในตารางหลัก ซึ่งการทำในลักษณะนี้เราเรียกกันว่า Master-Detail นะครับ ส่วนวิธีการนั้นก็มีวิธีการทำได้หลากหลายนะครับตั้งแต่ แบบลูกทุ่ง ไปจนถึง Best Practices ในบทความนี่เราจะหล่าวถึงวิธีที่การทำโดยใช้ component ที่ชื่อว่า BindingSource ถ้าใครเป็นแฟนของ ADO.NET ก็คงพอจะรู้ว่า มันเป็น component ที่เริ่มมีใช้ใน version 2.0 นะครับ ถ้าผมไม่เลอะเลือน  ซึ่งวิธีการนี้จะทำให้ programmer ลดงานต่าง ๆ ลงมากและ ลดข้อผิดพลาดลงได้มากนะครับ ไม่ต้องเสียเวลาไปพะวงกับการ ตรวจสอบมากจนเกินไป

สำหรับการทำงานผมจะกล่าวถึง concept ในการทำก่อน นะครับ ตัวอย่างของแนวคิดนี้ก็เช่น เรามีตาราง Orders และ OrderDetails โดยทั้งสองตารางนี้เราอ้างอิงกันโดยใช้ OrderID นะครับ ดังรูป

pic2

แล้วใน windows Form นั้นเราต้องการแสดงผลดังนี้

dotnet_databinding_3

นั่นก็คือเราใช้ DataGridView กับทั้งสอง table แล้ว Binding ด้วย BindingSource ให้กับทั้งสอง table ซึ่งเดี๋ยวเราจะมาดูกันว่าเราจะ Binding อย่างไร และเราก็จะต้อง สร้าง Relation ของทั้งสองตารางนี้ ใน DataSet ที่เรา Fill ทั้งสองตารางนี้ลงไปด้วย คือเราจะทำการ Fill ทั้งสอง Table ลงใน Dataset เดียวกันแล้วก็สร้าง Relation ระหว่างทั้งสอง Tables นี้แล้วจึง ทำการ Binding ให้กับ DataGridView นะครับ นั่นคือ Concept ทั้งหมดในการทำ ต่อไปเรามาดูขั้นตอนวิธีการทำเลยดีกว่านะครับ

สมมุติว่าเราได้สร้าง windows Form แล้วและเราได้ นำเอา DataGridView มาวางใน Form แล้วตั้งชื่อให้ว่าเป็น dgvOrders และ dgvOrderDetails นะครับ  อีกนิดหนึ่งนะครับ ฐานข้อมูลถ้าเราใช้ Microsoft Access เราต้อง  using System.Data.OleDb หรือ  ถ้าเราใช้ Microsoft SQLServer เราต้อง using System.Data.SqlClient นะครับ

  • เราก็จะต้องประการตัวแปร object ต่าง ที่เราจะต้องใช้งานเสียก่อนนะครับ   เช่น  OleDbConnection, OleDbCommand, OleDbDataAdapter, BindingSource
  • .
    .
    using System.Data.OleDb;

    namespace WindowsApplication1
    {
    public partial class Form1 : Form
    {
    OleDbConnection myConnection;
    OleDbCommand myCommand;
    OleDbDataAdapter myDataAdapter;
    BindingSource orders_BindingSource;
    BindingSource ordersDetail_BindingSource;
    DataSet myDataset;
    public Form1()
    {
    InitializeComponent();
    }
    private void Form1_Load(object sender, EventArgs e)
    {

    }
    .
    .
    .
    }

  • ต่อไป เราจะทำการ Binding ใน  Form1_load นะครับ ในที่นี้ผมจะไม่กล่าวถึง Connection String นะครับ ทุกท่านคงจะรู้แล้วนะครับ  เราจะกำหนด Sql String ไว้สองชุดเพื่อการเข้าถึงข้อมูลในแต่ละ Table จะใช้ Sql String อันเดียวก็ได้นะครับ ผมใช้สองชุดก็เพื่อให้การอธิบายมันชัดเจน แล้วเราจะใช้  myCommand กำหนด SelectCommand ให้กับ myDataAdapter และ เราก็จะ Fill ลงใน DataSet ซึ่งเรา จะทำการ Fill ลงไปสองครั้ง ดูตามตัวอยางก็แล้วกันนะครับ private void Form1_Load(object sender, EventArgs e)
            {
                string ConnectionString = GetConnectionString();
                myConnection = new OleDbConnection(ConnectionString);
                myConnection.Open();

                string SqlString1 = “Select * from Orders”;
                string SqlString2 = “Select * from OrderDetails”;

                myCommand = myConnection.CreateCommand();
                myCommand.CommandType = CommandType.Text;
                myCommand.CommandText = SqlString1;

                myDataAdapter = new OleDbDataAdapter();
                myDataAdapter.SelectCommand = myCommand;

                myDataSet = new DataSet();

                myDataAdapter.Fill(myDataSet, “Orders”);

                myCommand.CommandText = SqlString2;

                myDataAdapter.Fill(myDataSet, “OrderDetails”);

            }

  • ครับต่อไปเราจะสร้าง Relation ระหว่าง 2 Tables นี้นะครับ เราจะตั้งชื่อ Relation นี้ว่า เป็น Oreder_OrderDetail นะครับ  และเราก็จะใช้ OrderID เป็น ตัวข้อมูลอ้างอิงของทั้งสองตารางดังนี้นะครับดูตามตัวอย่างเลย นะครับ private void Form1_Load(object sender, EventArgs e)
    {
    .
    .
    .
                myDataAdapter.Fill(myDataSet, “Orders”);

                myCommand.CommandText = SqlString2

                myDataAdapter.Fill(myDataSet, “OrderDetails”);
                // —- สร้าง relation —–
                DataRelation myDataRelation = new DataRelation(“Order_OrderDetails”,
                                              myDataSet.Tables[“Orders”].Columns[“OrderID”],
                                              myDataSet.Tables[“OrderDetails”].Columns[“OrderID”]);
                //————————
    orders_BindingSource = new BindingSource();
    ordersDetail_BindingSource = new BindingSource();

    // –กำหนด DataSource ให้กับ Bindingsource
    orders_BindingSource.DataSource = myDataSet;
    orders_BindingSource.DataMember = “Orders”;

    ordersDetail_BindingSource.DataSource = orders_BindingSource;
    ordersDetail_BindingSource.DataMember = “Order_OrderDetails”;

    // –Binding ให้กับ Datagridview ทั้งสอง
    dgvOrders.DataSource = orders_BindingSource;
    dgvOrderDetails.DataSource = ordersDetail_BindingSource;
            }

  • ต่อไปทำ การสร้าง BindingSource และกำหนด DataSource ให้กับ BindingSource ทั้งสองนะครับ และก็ต่อไปเลยก็คือ Binding ให้กับ DataGridView ทั้งสองด้วย ดูต่อจาก code ด้านบนนะครับ…
  • ครับ ถ้าดูจากตัวอย่าง code แล้วคงจะพอเข้าใจได้ไม่ยากนะครับ ทั้งหมดนี้ก็เป็นการ ทำการ Binding ในลักษณะ Master-Details ซึ่งผมเคยลองหาใน internet แล้วก็ค่อนข้างที่หายากนิดหนึ่งไม่ค่อยมีใครอธิบายละเอียดละออเป็นภาษาไทยนะครับ ผมก็พยายามอธิบายให้ละเอียดเพื่อเพื่อน โปรแกรมเมอร์คนไทยด้วยกัน  ในเรื่องของการ Binding แบบนี้ นะครับ เป็นคุณลักษณะที่ ใช้กับ ADO.NET 2.0 ขึ้นไปนะครับ รู้สึกว่าถ้าต่ำกว่านี้เขาจะไม่มี  BindingSource ครับ  จริงแล้วการ Binding นั้นยังมีอีกมากนะครับ ในตัวอย่างที่ผมกล่าวมาแล้วนี้เป็นลักษณะ  one to many นะครับ ยังมีแบบ many to many อีก ซึ่งคงต้องเอาไว้มีเวลาแล้วจะมาลงไว้ให้นะครับ  หรือถ้าใครมีแหล่งข้อมูลก็ช่วย post comment ให้ด้วยนะครับเพื่อ เป็นวิทยาทาน ให้กับเพื่อนโปรแกรมเมอร์ด้วยกัน นะครับ  s_teerapong2000@yahoo.com
  • สำหรับหนังสือที่เรียกได้ว่าเป็นสุดยอดของเรื่องนี้ก็ต้องเล่มนี้นะครับ  “Data Binding with Windows Forms 2.0 – Programming Smart Client Data Applications with .NET” นะครับ