Web-Based Security in Commerce Server 2002
Published: July 1, 2002
On This Page
Introduction
User Input Vulnerabilities
Using Managed Code to Identify Trusted Input
Security Vulnerability: Unbounded Sizes
Security Vulnerability: Using Direct User Input in SQL Statements
User Input Remedies
Introduction
One of the most common mistakes made by vendors of Web-based servers and Web-based applications is trusting users to send well-formed, non-malicious data. When you accept input from users, either directly or indirectly, it is imperative that you validate the input before using it. Data must be validated as it crosses the boundary between untrusted and trusted environments. By definition, trusted data is data you or an entity you explicitly trust has complete control over; untrusted data refers to everything else. Any data submitted by a user is initially untrusted data.
This white paper focuses on Web-based security vulnerabilities. It includes content, used with permission from the authors, from the excellent book, Writing Secure Code, by Michael Howard and David LeBlanc (Microsoft Press, 2002). Before you begin developing your Commerce Server site, it is strongly recommended that you read Writing Secure Code.
Top of page
User Input Vulnerabilities
All Web applications perform some action based on user requests. To protect your site from attack in the form of user input, you must determine what is valid data and reject all other input.
The following example is based on Active Server Pages (ASP) code from a Web site that recommended Web site designers use the following Microsoft® JScript® code in their ASP-based applications to implement forms-based authentication:
// Get the username and password from the form.
if (isValidUserAndPwd(Request.form("name"),
Request.form("pwd"))) {
Response.write("Authenticated!");
} else {
Response.write("Access Denied");
}
function isValidUserAndPwd(strName, strPwd) {
var fValid = false;
var oConn = new ActiveXObject("ADODB.Connection");
oConn.Open("Data Source=c:\\auth\\auth.mdb;");
var strSQL = "SELECT count(*) FROM client WHERE " +
"name='" + strName + "' " +
" and pwd='" + strPwd + "'";
var oRS = new ActiveXObject("DODB.RecordSet");
oRS.Open(strSQL, oConn);
fValid = (oRS(0).Value > 0) ? true : false;
oRS.Close();
delete oRS;
oConn.Close();
delete oConn;
return fValid;
}
Below is the client code used to send the username and password to the JScript code by using an HTTP POST:
<FORM ACTION="Logon.asp" METHOD=POST>
<INPUT TYPE=text MAXLENGTH=32 NAME=name>
<INPUT TYPE=password MAXLENGTH=32 NAME=pwd>
<INPUT TYPE=submit NAME=submit VALUE="Logon">
</FORM>
In this example, the user enters a username and a password by using the HTML form shown above and then clicks the Logon button. The ASP code uses the provided username and password to build a SQL statement to query a database. If the number of rows returned by the query is greater than zero—SELECT count(*) returns the number of rows returned by the SQL query—then the username and password combination are valid and the user is allowed to log on to the system.
Both the client and server code are flawed, however, because the solution takes direct user input and uses it to access a database without checking whether the input is valid. In other words, data is transferred from an untrusted source—a user—to a trusted source, the SQL database under application control.
Assume a non-malicious user enters his name, "Blake," and the password "$qu1r+," which builds the following SQL statement:
SELECT count(*) FROM client
WHERE name='Blake'
AND pwd='$qu1r+'
If this is a valid username and password combination, count(*) returns a value of at least 1 and allows the user access to the system. The query could potentially return more than 1 if two users exist with the same username and password or if an administrative error leads to the data being entered twice.
Because the username and password are not checked by the ASP application, an attacker can send any input.
Do Not Trust User Input Directly
You should never trust user input directly, especially if the user input is anonymous. Remember these two golden rules:
One malicious user input scenario to be wary of occurs when your application accepts user input, and then uses the input to create output for other users. For example, consider the security ramifications if you build a Web service that allows users to create and post product reviews for other users of the system to read prior to making a product purchase. Imagine that an attacker does not like ProductA but likes ProductB. The attacker creates a comment about ProductA, which will appear on the ProductA Web page, along with all the other reviews. However, the comment is this:
<meta http-equiv="refresh"
content="2;URL=http://www.northwindtraders.com/productb.aspx">
This HTML code will send the user's browser to the product page for ProductB after the browser has spent two seconds at the page for ProductA!
Top of page
Using Managed Code to Identify Trusted Input
Managed code helps mitigate a number of common security vulnerabilities, such as buffer overruns, and some of the issues associated with fully trusted mobile code, such as Microsoft®ActiveX® controls. Traditional security in Microsoft® Windows® takes only the principal's identity into consideration when performing security checks. In other words, if the user is trusted, the code runs with that person's identity and therefore is trusted and has the same privileges as the user.
The following code shows an example of trusting input using managed code.
using System.Data.SQLClient;
using System.Data.SQLTypes;public static SqlMoney FreightByCargo(string company) {
SqlCommand cmd = new SqlCommand();
cmd.CommandText = "select sum(cost) as cost " +
"from orders " +
"where companyname = '" + company + "'"; return cmd.ExecuteScalar();
}
The following examples show why this code sample is wrong, and how attackers can exploit it.
User input: An example of the valid input you expect
Name: Foo Corp
SELECT sum(cost)
FROM orders
WHERE companyname='Foo Corp'
Attacker input: What an attacker actually inputs
Name: 'Test' or 1=1 --
SELECT sum(cost)
FROM orders
WHERE companyname='Test' or 1=1 -- '
In the above example, the bad input is 'Test' or 1=1 -- '. This input is bad because the attacker can use this to test whether SQL statements can be injected into the code using a plain user level privilege.
Very Bad Attacker Input
Name: b' drop table orders --
SELECT sum(cost)
FROM orders
WHERE companyname= 'b' drop table orders -- '
In the above example, the bad input is b' drop table orders -- . The input is bad because it allows the attacker to inject a SQL statement into drop tables. To accomplish this, however, the attacker must have db_owner level privileges.
Very, Very Bad Attacker Input
Name: b' xp_cmdshell('fdisk.exe') --
SELECT sum(cost)
FROM orders
WHERE companyname= 'b' xp_cmdshell('fdisk.exe') -- '
In the above example, the bad input is b' xp_cmdshell('fdisk.exe') – . This input is bad because the attacker can use it to execute system-level commands (from shell to command line mode). To accomplish this, however, the attacker must be able to connect to your Microsoft® SQL Server™ databases using the sa level privilege.
A More Secure Managed Code Example
The following code sample shows how you can better prevent the above attacks from succeeding.
using System.Data.SqlServer;
using System.Data.SqlTypes; public static SqlMoney FreightByCargo(string company) {
SqlCommand cmd = new SqlCommand();
cmd.CommandText = "select sum(cost) as cost" +
"from orders " +
"where companyname = @CompanyName";
SqlParameter param = cmd.Parameters.Add("@CompanyName",
company);
return cmd.ExecuteScalar( );
}
Top of page
Security Vulnerability: Unbounded Sizes
If the size of the client data is unbounded and unchecked, attackers can send as much data as they want. This could be a security issue if there exists an as-yet unknown buffer overrun in the database code called when the SQL query is invoked. On closer examination, an attacker can easily bypass the maximum username and password size restrictions imposed by the previous client HTML form code, which restricts both fields to 32 characters, simply by not using the client code. Instead, attackers write their own client code in, for example, Perl, or just use a Telnet client.
The following is such an example, which sends a valid HTML form to Logon.asp but sets the password and username to be 32,000 letter "A"s.
use HTTP::Request::Common qw(POST GET); use LWP::UserAgent; $ua = LWP::UserAgent->new(); $req = POST 'http://www.northwindtraders.com/Logon.asp', [ pwd => 'A' x 32000, name => 'A' x 32000, ]; $res = $ua->request($req);
Do not rely on client-side HTML security checks—in this case, by thinking that the username and password lengths are restricted to 32 characters—because an attacker can always bypass such controls by bypassing the client altogether.
Top of page
Security Vulnerability: Using Direct User Input in SQL Statements
Because the input is untrusted and has not been checked for validity, an attacker could change the semantics of a SQL statement. In the following example, the attacker enters a completely invalid name and password, both of which are "b" or "1" = "1," which builds the following valid SQL statement:
SELECT count(*)
FROM client
WHERE name='b' or '1'='1' and pwd='b' or '1'='1'
This statement will always return a row count value of greater than one, because the "'1' = '1'" fragment is true on both sides of the "and" clause. The attacker is authenticated without knowing a valid username or password—the input entered simply changed the way the SQL query works.
Here is another variation: an attacker who knows a username and wants to spoof that user account can do this using SQL comments—for example, two hyphens (--) in Microsoft SQL Server or the hash sign (#) in mySQL. Some other databases use the semicolon (;) as the comment symbol. Rather than entering "'b' or '1' = '1,'" the attacker enters "Cheryl' --,"which builds up the following legal SQL statement:
SELECT count(*)
FROM client
WHERE name='Cheryl' --and pwd=''
If a user named Cheryl is defined in the system, the attacker can log in because he has commented out the rest of the SQL statement that evaluates the password, so that the password is not checked.
SQL statements can be joined. For example, the following SQL statement is valid:
SELECT * from client INSERT into client VALUES ('me', 'URHacked')
This single line contains two SQL statements. The first selects all rows from the client table, and the second inserts a new row into the same table.
Note: One reason the INSERT statement might work for an attacker is that most people connect to SQL databases by using elevated accounts, such as the sysadmin account (sa) in SQL Server. This is yet another reason to use least-privilege principles when designing Web applications.
An attacker could use this login ASP page and enter a username of "'b' INSERT INTO client VALUES ('me', 'URHacked') --," which would build the following SQL statement:
SELECT count(*)
FROM client
WHERE name='b' INSERT INTO client VALUES ('me', 'URHacked') --and pwd=''
The password is not checked, because that part of the query is commented out. And the attacker has added a new row containing "me" as a username and "URHacked" as the password—now the attacker can log in using "me" and "URHacked."
Top of page
User Input Remedies
As with all user input issues, the first rule is to determine which input is trusted and to reject all other input. Other options exist and offer more functionality with potentially less security.
The following techniques reduce the threat of user input:
- Determine what is valid input and reject everything else.
- Use regular expressions.
- Display user input only after sanitizing it.
- Passwords are problematic: escape them using Server.URLEncode or HttpServerUtility.URLEncode.
- Never use the sa account to log on to SQL Server from any application. It defeats the least-privilege principle.
- Do not construct SQL strings.
- Use placeholders or stored procedures.
Input Testing Ideas
Before you deploy your site, it is recommended that you enumerate all entry points to the application: sockets, remote procedure calls (RPCs), named pipes, files, registry keys, SOAP parameters, HTTP headers, form values, query strings, and so on. Then, use a tool that goes "under the radar" such as Perl, C#, or C++. Create input that is too big, too small, non-existent, or the wrong data type, and test it.
Insist on Valid Input
The valid characters for a user name can be easily restricted to a small set of valid characters, such as A-Z, a-z, and 0-9. The following server-side JScript snippet shows how to construct and use a regular expression to parse the user name at the server:
// Determine whether user name is valid.
// Valid format is 1 to 32 alphanumeric characters.
var reg = /^[A-Za-z0-9]{1,32}$/g;
if (reg.test(Request.form("name")) > 0) {
// Username is valid.
} else {
// Username is invalid.
}
Note the use of the 'g' option at the end of the expression just shown. This is the global option that forces the regular expression to check all input for the pattern; otherwise, it checks the first line only. Not setting the global option can have serious consequences if the attacker can force the input to span multiple lines.
Not only does this regular expression restrict the username to a small subset of characters, but also it verifies that the string is between 1 and 32 characters long.
Note: Note the use of "^" and "$" in the regular expression—these signify that all characters from the start (^) to the end ($) of the input must match this regular expression. Otherwise, the regular expression might match only a subset of the string. For example, /[A-Za-z0-9]{1,32}/ would only match any portion of the input string. And HTML tags or script, such as <script>alert("hi!")</script>, would match because the word "script" matches the expression.
Your code should apply a regular expression to all input, whether it is part of a form, an HTTP header, or a query string.
In the case of the file name passed to the Web server as a query string, the following regular expression, which represents a valid file name (this does not allow for directories or drive letters), would defeat any attempt to use script as part of the query string:
// Determine whether filename is valid.
// Valid format is 1 to 24 alphanumeric characters
// followed by a period, and 1 to 3 alpha characters.
var reg = /^[A-Za-z0-9]{1,24}\.[A-Za-z]{1,3}$/g;
if (reg.test(Request.Querystring("file")) > 0) {
// Valid filename.
} else {
// Invalid filename.
}
A common mistake made by many Web developers is to allow "safe" HTML constructs—for example, allowing a user to send <IMG> or <TABLE> tags to the Web application. Then the user can send HTML tags but nothing else, other than plain text. Do not do this. A cross-site scripting danger still exists because the attacker can embed script in some of these tags.
Following are some examples:
<img src=javascript:alert(document.domain)>
<link rel=stylesheet href="javascript:alert(document.domain)">
<input type=image src=javascript:alert(document.domain)>
<bgsound src=javascript:alert(document.domain)>
<iframe src="javascript:alert(document.domain)">
<frameset onload=vbscript:msgbox(document.cookie)></frameset>
<table background="javascript:alert(document.domain)"></table>
<object type=text/html data="javascript:alert(document.domain);"></object>
<body onload="javascript:alert(document.cookie)"></body>
<body background="javascript:alert(document.cookie)"></body>
<p style=left:expression(alert(document.cookie))>
Top of page