Добавил:
Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:

Pro ASP.NET 2.0 In CSharp 2005 (2005) [eng]

.pdf
Скачиваний:
92
Добавлен:
16.08.2013
Размер:
29.8 Mб
Скачать

248 C H A P T E R 7 A D O. N E T F U N D A M E N TA L S

SQL Injection Attacks

So far, all the examples you’ve seen have used hard-coded values. That makes the examples simple, straightforward, and relatively secure. It also means they aren’t that realistic, and they don’t demonstrate one of the most serious risks for web applications that interact with a database—SQL injection attacks.

In simple terms, SQL injection is the process of passing SQL code into an application, in a way that was not intended or anticipated by the application developer. This may be possible because of the poor design of the application, and it affects only applications that use SQL string building techniques to create a command with user-supplied values.

Consider the example shown in Figure 7-5. In this example, the user enters a customer ID, and the GridView shows all the rows for that customer. In a more realistic example the user would also need to supply some sort of authentication information such as a password. Or, the user ID might be based on a previous login screen, and the text box would allow the user to supply additional criteria such as a date range or the name of a product in the order.

Figure 7-5. Retrieving orders for a single customer

The problem is how this command is executed. In this example, the SQL statement is built dynamically using a string building technique. The value from the txtID text box is simply pasted into the middle of the string. Here’s the code:

string connectionString = WebConfigurationManager.ConnectionStrings["Northwind"].ConnectionString;

SqlConnection con = new SqlConnection(connectionString); string sql =

"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 = '" + txtID.Text + "' " + "GROUP BY Orders.OrderID, Orders.CustomerID";

SqlCommand cmd = new SqlCommand(sql, con);

C H A P T E R 7 A D O. N E T F U N D A M E N TA L S

249

con.Open();

SqlDataReader reader = cmd.ExecuteReader(); GridView1.DataSource = reader; GridView1.DataBind();

reader.Close();

con.Close();

In this example, a user might try to tamper with the SQL statement. Often, the first goal of such an attack is to receive an error message. If the error isn’t handled properly and the low-level information is exposed to the attacker, that information can be used to launch a more sophisticated attack.

For example, imagine what happens if the user enters the following text into the text box:

ALFKI' OR '1'='1

Now consider the complete SQL statement that this creates:

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

This statement returns all the order records. Even if the order wasn’t created by ALFKI, it’s still true that 1=1 for every row. The result is that instead of seeing the specific information for the

current customer, all the information is exposed to the attacker, as shown in Figure 7-6. If the information shown on the screen is sensitive, such as Social Security numbers, dates of birth, or credit card information, this could be an enormous problem! In fact, simple SQL injection attacks exactly like this are often the source of problems that affect major e-commerce companies. Often, the vulnerability doesn’t occur in a text box but appears in the query string (which can be used to pass a database value such as a unique ID from a list page to a details page).

Figure 7-6. A SQL injection attack that shows all rows

250 C H A P T E R 7 A D O. N E T F U N D A M E N TA L S

More sophisticated attacks are possible. For example, the malicious user could simply comment out the rest of your SQL statement by adding two hyphens (--).This attack is specific to SQL Server, but equivalent exploits are possible in MySQL with the hash (#) symbol and in Oracle with the semicolon (;). Or the attacker could use a batch command to execute an arbitrary SQL command. With the SQL Server provider, the attacker simply needs to supply a semicolon followed by a new command. This exploit allows the user to delete the contents of another table, or even use the SQL Server xp_cmdshell system stored procedure to execute an arbitrary program at the command line.

Here’s what the user would need to enter in the text box for a more sophisticated SQL injection attack to delete all the rows in the Customers table:

ALFKI'; DELETE * FROM Customers--

So, how can you defend against SQL injection attacks? You can keep a few good guidelines in mind. First, it’s a good idea to use the TextBox.MaxLength property to prevent overly long entries if they aren’t needed. That reduces the chance of a large block of script being pasted in where it doesn’t belong. In addition, you should restrict the information given by error messages. If you catch a database exception, you should report only a generic message like “Data source error”

rather than display the information in the Exception.Message property, which may indicate system vulnerabilities.

More important, you should take care to remove special characters. For example, you can convert all single quotation marks to two quotation marks, thereby ensuring that they won’t be confused with the delimiters in your SQL statement:

string ID = txtID.Text().Replace("'", "''");

Of course, this introduces headaches if your text values really should contain apostrophes. It also suffers because some SQL injection attacks are still possible. Replacing apostrophes prevents a malicious user from closing a string value prematurely. However, if you’re building a dynamic SQL statement that includes numeric values, a SQL injection attack just needs a single space. This vulnerability is often (and dangerously) ignored.

An even better approach is to use a parameterized command or a stored procedure that performs its own escaping and is impervious to SQL injection attacks. The following sections describe these techniques.

Tip Another good idea is to restrict the permissions of the account used to access the database so that it doesn’t have the right to access other databases or execute extended system stored procedures. However, this can’t remove the problem of SQL script injection, because the process you use to connect to the database will almost always require a broader set of privileges than the ones you would allocate to any single user. By restricting the account, you could prevent an attack that deletes a table, for example, but you probably can’t prevent an attack that steals someone else’s information.

Using Parameterized Commands

A parameterized command is simply a command that uses placeholders in the SQL text. The placeholders indicate dynamically supplied values, which are then sent through the Parameters collection of the Command object.

For example, this SQL statement:

SELECT * FROM Customers WHERE CustomerID = 'ALFKI'

would become something like this:

SELECT * FROM Customers WHERE CustomerID = @CustID

C H A P T E R 7 A D O. N E T F U N D A M E N TA L S

251

The placeholders are then added separately and automatically encoded.

The syntax for parameterized commands differs slightly for different providers. With the SQL Server provider, parameterized commands use named placeholders (with unique names). With the OLE DB provider, each hard-coded value is replaced with a question mark. In either case, you need to supply a Parameter object for each parameter, which you insert into the Command.Parameters collection. With the OLE DB provider, you must make sure you add the parameters in the same order that they appear in the SQL string. This isn’t a requirement with the SQL Server provider, because the parameters are matched to the placeholders based on their names.

The following example rewrites the query to remove the possibility of a SQL injection attack:

string connectionString = WebConfigurationManager.ConnectionStrings["Northwind"].ConnectionString;

SqlConnection con = new SqlConnection(connectionString); string sql =

"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 = @CustID " +

"GROUP BY Orders.OrderID, Orders.CustomerID"; SqlCommand cmd = new SqlCommand(sql, con); cmd.Parameters.Add("@CustID", txtID.Text);

con.Open();

SqlDataReader reader = cmd.ExecuteReader(); GridView1.DataSource = reader; GridView1.DataBind();

reader.Close();

con.Close();

If you try to perform the SQL injection attack against this revised version of the page, you’ll find it returns no records. That’s because no order items contain a customer ID value that equals the text string ALFKI' OR '1'='1. This is exactly the behavior you want.

Calling Stored Procedures

Parameterized commands are just a short step from commands that call full-fledged stored procedures.

A stored procedure, of course, is a batch of one or more SQL statements that are stored in the database. Stored procedures are similar to functions in that they are well-encapsulated blocks of logic that can accept data (through input parameter) and return data (through result sets and output parameters). Stored procedures have many benefits:

They are easier to maintain. For example, you can optimize the commands in a stored procedure without recompiling the application that uses it.

They allow you to implement more secure database usage. For example, you can allow the Windows account that runs your ASP.NET code to use certain stored procedures but restrict access to the underlying tables.

They can improve performance. Because a stored procedure batches together multiple statements, you can get a lot of work done with just one trip to the database server. If your database is on another computer, this reduces the total time to perform a complex task dramatically.

252 C H A P T E R 7 A D O. N E T F U N D A M E N TA L S

Note SQL Server version 7 (and later) precompiles all SQL commands, including off-the-cuff SQL statements. That means you gain the benefit of compilation regardless of whether you are using stored procedures. However, stored procedures still tend to increase the performance benefits, because they limit the variation in SQL statements, thereby ensuring that a single compiled execution plan can be reused more often and more effectively. Also, because the database code is contained in the database, not the client, it’s easier for a database administrator to fine-tune indexes and locks and to employ other optimization strategies.

Here’s the SQL code needed to create a stored procedure for inserting a single record into the Employees table. This stored procedure isn’t in the Northwind database initially, so you’ll need to add it to the database (using a tool such as Enterprise Manager or Query Analyzer) before you use it.

CREATE PROCEDURE InsertEmployee @TitleOfCourtesy varchar(25), @LastName varchar(20), @FirstName varchar(10), @EmployeeID int OUTPUT

AS

INSERT INTO Employees

(TitleOfCourtesy, LastName, FirstName, HireDate)

VALUES (@TitleOfCourtesy, @LastName, @FirstName, GETDATE());

SET @EmployeeID = @@IDENTITY

GO

This stored procedure takes three parameters for the employee’s title of courtesy, last name, and first name. It returns the ID of the new record through the output parameter called @EmployeeID, which is retrieved after the INSERT statement using the @@IDENTITY function. This is one example of a simple task that a stored procedure can make much easier. Without using a stored procedure, it’s quite awkward to try to determine the automatically generated identity value of a new record you’ve just inserted.

Next, you can create a SqlCommand to wrap the call to the stored procedure. This command takes the same three parameters as inputs and uses @@IDENTITY to get and then return the

ID of the new record. Here is the first step, which creates the required objects and sets the InsertEmployee as the command text:

string connectionString = WebConfigurationManager.ConnectionStrings["Northwind"].ConnectionString;

SqlConnection con = new SqlConnection(connectionString);

// Create the command for the InsertEmployee stored procedure. SqlCommand cmd = new SqlCommand("InsertEmployee", con); cmd.CommandType = CommandType.StoredProcedure;

Now you need to add the stored procedure’s parameters to the Command.Parameters collection. When you do, you need to specify the exact data type and length of the parameter so that it matches the details in the database.

Here’s how it works for a single parameter:

cmd.Parameters.Add(new SqlParameter("@TitleOfCourtesy", SqlDbType.NVarChar, 25)); cmd.Parameters["@TitleOfCourtesy"].Value = title;

The first line creates a new SqlParameter object; sets its name, type, and size in the constructor; and adds it to the Parameters collection. The second line assigns the value for the parameter, which will be sent to the stored procedure when you execute the command.

C H A P T E R 7 A D O. N E T F U N D A M E N TA L S

253

Note Some providers include an overload to the Parameter.Add() method that allows you to create a parameter object without specifying the data type. However, this approach usually requires some degree of reflection, which means the data provider must query the data source to find out the parameter details. The best-performing approach is to specify the data type details in full, even though they make for tedious code.

Now you can add the next two parameters in a similar way:

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;

The last parameter is an output parameter, which allows the stored procedure to return information to your code. Although this Parameter object is created in the same way, you must make sure you specify it is an output parameter by setting its Direction property to Output. You don’t need to supply a value.

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

Finally, you can open the connection and execute the command with the ExecuteNonQuery() method. When the command is completed, you can read the output value, as shown here:

con.Open(); try

{

int numAff = cmd.ExecuteNonQuery(); HtmlContent.Text += String.Format(

"Inserted <b>{0}</b> record(s)<br />", numAff);

// Get the newly generated ID.

empID = (int)cmd.Parameters["@EmployeeID"].Value; HtmlContent.Text += "New ID: " + empID.ToString();

}

finally

{

con.Close();

}

In the next chapter, you’ll see a small but fully functional database component that does all its work through stored procedures.

Transactions

A transaction is a set of operations that must either succeed or fail as a unit. The goal of a transaction is to ensure that data is always in a valid, consistent state.

For example, consider a transaction that transfers $1,000 from account A to account B. Clearly there are two operations:

It should deduct $1,000 from account A.

It should add $1,000 to account B.

254 C H A P T E R 7 A D O. N E T F U N D A M E N TA L S

Suppose that an application successfully completes step 1, but, because of some error, step 2 fails. This leads to an inconsistent data, because the total amount of money in the system is no longer accurate. A full $1,000 has gone missing.

Transactions help avoid these types of problems by ensuring that changes are committed to a data source only if all the steps are successful. So, in this example, if step 2 fails, then the changes made by step 1 will not be committed to the database. This ensures that the system stays in one of its two valid states—the initial state (with no money transferred) and the final state (with money debited from one account and credited to another).

Transactions are characterized by four properties popularly called ACID properties. ACID is an acronym that represents the following concepts:

Atomic: All steps in the transaction should succeed or fail together. Unless all the steps from a transaction complete, a transaction is not considered complete.

Consistent: The transaction takes the underlying database from one stable state to another.

Isolated: Every transaction is an independent entity. One transaction should not affect any other transaction running at the same time.

Durable: Changes that occur during the transaction are permanently stored on some media, typically a hard disk, before the transaction is declared successful. Logs are maintained so that the database can be restored to a valid state even if a hardware or network failure occurs.

Note that even though these are ideal characteristics of a transaction, they aren’t always absolutely attainable. One problem is that in order to ensure isolation, the RDBMS needs to lock data so that other users can’t access it while the transaction is in progress. The more locks you use, and the coarser these locks are, the greater the chance that a user won’t be able to perform another task while the transactions are underway. In other words, there’s often a trade-off between user concurrency and isolation.

Transactions and ASP.NET Applications

You can use three basic transaction types in an ASP.NET web application. They are as follows (from least to most overhead):

Stored procedure transactions: These transactions take place entirely in the database. Stored procedure transactions offer the best performance, because they need only a single round-trip to the database. The drawback is that you also need to write the transaction logic using SQL statements (which may be not as easy as using pure C#).

Client-initiated (ADO.NET) transactions: These transactions are controlled programmatically by your ASP.NET web-page code. Under the covers, they use the same commands as a stored procedure transaction, but your code uses some ADO.NET objects that wrap these details. The drawback is that extra round-trips are required to the database to start and commit the transaction.

COM+ transactions: These transactions are handled by the COM+ runtime, based on declarative attributes you add to your code. COM+ transactions use a two-stage commit protocol and always incur extra overhead. They also require that you create a separate serviced component class. COM+ components are generally a good choice only if your transaction spans multiple transaction-aware resource managers, because COM+ includes built-in support for distributed transactions. For example, a single COM+ transaction can span interactions in a SQL Server database and an Oracle database. COM+ transactions are not covered in this chapter, although you will consider them briefly with web services in Chapter 32.

C H A P T E R 7 A D O. N E T F U N D A M E N TA L S

255

Note ADO.NET 2.0 introduces a new concept of promotable transactions. However, a promotable transaction isn’t a new type of transaction—it’s just a way to create a client-initiated transaction that can automatically escalate itself into a COM+ transaction if needed. You shouldn’t use promotable transactions unless you need to, as they make it more difficult to predict the performance and scalability of your solution. You can learn more about promotable transactions in Pro ADO.NET 2.0 (Apress, 2005).

Even though ADO.NET provides good support for transactions, you should not always use transactions. In fact, every time you use any kind of transaction, you automatically incur some overhead. Also, transactions involve some kind of locking of table rows. Thus, unnecessarily using transactions may harm the overall scalability of your application.

When implementing a transaction, you can follow these practices to achieve the best results:

Keep transactions as short as possible.

Avoid returning data with a SELECT query in the middle of a transaction. Ideally, you should return the data before the transaction starts.

If you do retrieve records, fetch only the rows that are required so as to not lock too many resources and so as to keep performance as good as possible.

Wherever possible, write transactions within stored procedures instead of using ADO.NET transactions.

Avoid transactions that combine multiple independent batches of work. Put separate batches into separate transactions.

Avoid updates that affect a large range of records if at all possible.

Tip As a rule of thumb, use a transaction only when your operation requires one. For example, if you are simply selecting records from a database, or firing a single query, you will not need a transaction. On the other hand, if you are inserting an Order record in conjunction with a series of related OrderItem records, you might want to

use a transaction. In general, a transaction is never required for single-statement commands such as individual UPDATE, DELETE, or INSERT statements, because these are inherently transactional.

Stored Procedure Transactions

If possible, the best place to put a transaction is in stored procedure code. This ensures that the server-side code is always in control, which makes it impossible for a client to accidentally hold a transaction open too long and potentially cause problems for other client updates. It also ensures the best possible performance, because all actions can be executed at the data source without requiring any network communication. Generally, the shorter the span of a transaction, the better the concurrency of the database and the fewer the number of database requests that will be serialized (put on hold while a temporary record lock is in place).

Stored procedure code varies depending on the database you are using, but most RDBMSs support the SQL statement BEGIN TRANSACTION. Once you start a transaction, all subsequent statements are considered part of the transaction. You can end the transaction with the COMMIT or ROLLBACK statement. If you don’t, the transaction will be automatically rolled back.

Here’s a pseudocode example that performs a fund transfer between accounts. It’s a simplified version that allows an account to be set to a negative balance.

256C H A P T E R 7 A D O. N E T F U N D A M E N TA L S

CREATE Procedure TransferAmount

(

@Amount Money @ID_A int #ID_B int

)

AS

BEGIN TRANSACTION

UPDATE Accounts SET Balance = Balance + @Amount WHERE AccountID = @ID_A UPDATE Accounts SET Balance = Balance - @Amount WHERE AccountID = @ID_B

IF (@@ERROR > 0) ROLLBACK

ELSE COMMIT

Note In SQL Server, a stored procedure can also perform a distributed transaction (one that involves multiple data sources and is typically hosted on multiple servers). By default, every transaction begins as a local transaction, but if you access a database on another server, the transaction is automatically upgraded to a distributed transaction governed by the Windows DTC (Distributed Transaction Coordinator) service.

Client-Initiated ADO.NET Transactions

Most ADO.NET data providers include support for database transactions. Transactions are started through the Connection object by calling the BeginTransaction() method. This method returns a provider-specific Transaction object that’s used to manage the transaction. All Transaction classes implement the IDbTransaction interface. Examples include SqlTransaction, OleDbTransaction, OracleTransaction, and so on.

The Transaction class provides two key methods:

Commit(): This method identifies that the transaction is complete and that the pending changes should be stored permanently in the data source.

Rollback(): This method indicates that a transaction was unsuccessful. Pending changes are discarded, and the database state remains unchanged.

Typically, you use Commit() at the end of your operation. However, if any exception is thrown along the way, you should call Rollback().

Here’s an example that inserts two records into the Employees table:

string connectionString = WebConfigurationManager.ConnectionStrings["Northwind"].ConnectionString;

SqlConnection con = new SqlConnection(connectionString);

SqlCommand cmd1 = new SqlCommand(

"INSERT INTO Employees (LastName, FirstName) VALUES ('Joe','Tester')"); SqlCommand cmd2 = new SqlCommand(

"INSERT INTO Employees (LastName, FirstName) VALUES ('Harry','Sullivan')"); SqlTransaction tran = null;

try

{

// Open the connection and create the transaction. con.Open();

tran = con.BeginTransaction();

C H A P T E R 7 A D O. N E T F U N D A M E N TA L S

257

//Enlist two commands in the transaction. cmd1.Transaction = tran;

cmd2.Transaction = tran;

//Execute both commands. cmd1.ExecuteNonQuery(); cmd2.ExecuteNonQuery();

//Commit the transaction.

tran.Commit();

}

catch

{

// In the case of error, roll back the transaction. tran.Rollback();

}

finally

{

con.Close();

}

Note that it’s not enough to create and commit a transaction. You also need to explicitly enlist each Command object to be part of the transaction by setting the Command.Transaction property to the Transaction object. If you try to execute a command that isn’t a part of the current transaction while the transaction is underway, you’ll receive an error. However, in the future this object model might allow providers to support more than one simultaneous transaction on the same connection.

Tip Instead of using separate command objects, you could also execute the same object twice and just modify its CommandText property in between (if it’s a dynamic SQL statement) or the value of its parameters (if it’s a parameterized command). For example, if your command inserts a new record, you could use this approach to insert two records in the same transaction.

To test the rollback features of a transaction, you can insert the following line just before the Commit() method is called in the previous example:

throw new ApplicationException();

This raises an exception, which will trigger a rollback and ensure that neither record is committed to the database.

Although an ADO.NET transaction revolves around the Connection and Transaction objects, the underlying commands aren’t different from a stored procedure transaction. For example, when you call BeginTransaction() with the SQL Server provider, it sends a BEGIN TRANSACTION command to the database.

Tip A transaction should be completed as quickly as possible (started as late as possible and finished as soon as possible). Also, an active transaction puts locks on the various resources involved, so you should select only the rows you really require.