Tip of the Day #17: Duplicate input fields

Don’t allow duplicate input fields into your form.

The other day I was trying to debug a bug in an application that I maintain. The code created a set of pagination buttons at the top of the page with previous and next buttons. At some point a request had come in that the buttons needed to be replicated at the bottom of the page. Since the HTML was being built up in a string and dumped in a literal control in the first place the developer that was tasked with making the change just dumped the string into two literal controls, the original at the top of the page, and the new one at the bottom of the page. The previous and next buttons use hidden input field to tell the application which actual page number the buttons correspond to. And these were now duplicated and as a result the previous and next buttons ceased to work.

Here is an example of something similar:

<input id="first-hidden-field" value="123" type="hidden" name="some-name" />
<input id="submit-button" value="Submit" type="submit" />
<input id="second-hidden-field" value="456" type="hidden" name="some-name" />

When the form fields are returned to the application and the field “some-name” is queried the result back is a combination of the two fields with the duplicate name. In this case:

string someName = Request.Form["some-name"];

will result in the value of “123,456” being stored in the string. Basically, it is the comma separated form of all the input fields with the given name.

Visual Studio / SQL Server install order on Windows 7

Quite a while ago I blogged about the Visual Studio / SQL Server install order on Windows Vista. I’m about to go through a similar exercise on Windows 7 and given the issues I had then I thought that it would be only right to document the procedure in case any problems arose.

Last time, it would seem, the best solution was to install things in the order in which Microsoft released them with the notable exception of the operating system. So this time, that is the strategy that I’m going to take. Windows 7 is already installed on my laptop. Then I’m going to install Visual Studio 2008, then SQL Server 2008, then any patches for either and we’ll see how we get on.

I’m also going to ensure that I do NOT install SQL Server Express Edition on Visual Studio 2008 as I’ve had problems with that before. Essentially, the problem last time was that the SQL Server installer mistook Visual Studio’s SQL Server Express installation has having installed certain things. The SQL Server installation therefore didn’t want to repeat what it didn’t need to so it refused to install the client tools.

Install Order

  • Visual Studio 2008, excluding SQL Server 2005 Express Edition

Visual Studio 2008 Installer Removing SQL Express

  • MSDN Library (This is optional – I installed it because I’m occasionally developing on the road with no or limited connectivity)
  • Visual Studio 2008 Service Pack 1 (this is required in order to install SQL Server 2008 – the installation will fail otherwise)
  • SQL Server 2008 Developer Edition

sql-server-2008-compatibility-issues

  • Install SQL Server 2008 SP1

That’s it – Job done. And it only took me two attempts to get it right this time. My stumbling block here was the order in which I applied the service packs.

My First OpenRasta Project – Part 2 (Resource Templates)

To get started see part 1.

For this part the Invoice class has been expanded to include another property, you’ll see why in a moment. For now, it now looks like this:

public class Invoice
{
    public string Reference { get; set; }
    public DateTime Date { get; set; }
}

Up to this point we are just showing some simple XML based on one of the built in codecs that ship as part of OpenRasta. There is a single URI and it always returns the same thing. So far there is nothing much going on.

You can keep on adding resources to the ResourceSpace when you are configuring the site, but that is hardly a scalable solution when so many applications are based on dynamic data. You need a way to define a resource template.

The way this is done is by adding place holders into the URI. These placeholders are defined by a set of curly braces with a parameter name inside. This is a bit like the string.Format method, except you can use meaningful names instead of the ordinal position of the parameter.

The configuration of the ResourceSpace in the Configuration class is changed to:

ResourceSpace.Has.ResourcesOfType<Invoice>()
    .AtUri("/invoice/{reference}")
    .HandledBy<InvoiceHandler>()
    .AsXmlDataContract();

As you can see the only difference is that the parameter on the AtUri method is changed.

OpenRasta will then look in the handler for a method that matches the HTTP verb and the parameters defined in the template.

The InvoiceHandler now has a method that looks like this:

public Invoice Get(string reference)
{
    Invoice result = InvoiceRepository.GetInvoiceByReference(reference);
    return result;
}

Don’t worry about the InvoiceRepository. It simply exists to get an Invoice object from somewhere. It could be from a database, in memory, a file or wherever.

We can now go to the uri /invoice/123-ABC and get the output:

<?xml version="1.0" encoding="utf-8"?>
<Invoice xmlns:i="http://www.w3.org/2001/XMLSchema-instance"
         xmlns="http://schemas.datacontract.org/2004/07/MyFirstOpenRastaProject.Resources">
  <Date>2009-09-30T00:00:00+01:00</Date>
  <Reference>123-ABC</Reference>
</Invoice>

However, that’s not the whole story. You can do some pretty neat things with resource templates.

For example, if the method parameter on the resource handler is a DateTime object you can build up the URI template using the property names in the DateTime object. The template parameters will then be mapped to the properties in the DateTime object.

First the configuration has to be updated:

ResourceSpace.Has.ResourcesOfType<Invoice>()
    .AtUri("/invoice/{reference}")
    .And.AtUri("/invoice/{day}/{month}/{year}")
    .HandledBy<InvoiceHandler>()
    .AsXmlDataContract();

There is only one additional line here and that is to add a URI with a template containing the day, month and year. It is still the same type of resource and the code hasn’t changed. All that is new is the URI template. If you were to attempt to create a brand new ResourceSpace for the Invoice resource you’d get an error that the resource type was already registered in the system.

The InvoiceHandler class will need an additional method to handle the new template. The new method looks like this:

public Invoice Get(DateTime date)
{
    Invoice result = InvoiceRepository.GetInvoiceByDate(date);
    return result;
}

The result for the URI /invoice/29/09/2009 looks like this

<?xml version="1.0" encoding="utf-8"?>
<Invoice xmlns:i="http://www.w3.org/2001/XMLSchema-instance"
         xmlns="http://schemas.datacontract.org/2004/07/MyFirstOpenRastaProject.Resources">
  <Date>2009-09-29T00:00:00</Date>
  <Reference>3e5a4e9e-3b46-4b09-a8f8-45010411501b</Reference>
</Invoice>

My First OpenRasta Project – Part 1

On the OpenRasta Wiki there are some instructions on getting your project up and running the manual way, should you so wish. One of the new features introduced at the last beta was a Visual Studio 2008 project template, which installs as part of the binary distribution.

Once installed you can create an OpenRasta project by going to the “Visual C#” Project Types and selecting OpenRasta ASP.NET Application (3.5) from the templates on the right of the dialog.

OpenRasta: New Project

Once the project is created you’ll see that it has set the project up, added the references to the assemblies that it needs and created an initial handler, resource and views.

OpenRasta: Solution Explorer

Before continuing further a little explanation of what each of these things are is in order.

A resource is a source of information. It is referenced via a URI. This blog post is a resource, an image is a resource, an invoice is a resource. However, a resource does not imply any particular kind of representation. In terms of OpenRasta a resource is simply an object

A handler is an object that manages the interaction of the resources. In MVC parlance it would be the “C” or controller.

A view is a regular ASP.NET page that can be rendered via the WebFormsCodec. It is not compulsory to implement any views at all if you don’t need ASP.NET.

A codec is the class responsible for encoding and decoding the representation of a resource. The built in codecs are WebForms, JSON and two types of XML.

First Code

When you get started you’ll need to configure OpenRasta. It needs to know the details of the resources you want to expose and the handlers that can deal with those resources. To do that OpenRasta looks for a class in your project that implements the IConfigurationSource interface.

If you have two or more classes that implement this interface then the first one that is found will be used. As the project template already contains a Configuration class already set up and ready to go there is nothing additional to do other than set the configuration.

In the example I’m going to show, we will be rendering an invoice. So the configuration needs to look like this:

public class Configuration : IConfigurationSource
{
    public void Configure()
    {
        using (OpenRastaConfiguration.Manual)
        {
            ResourceSpace.Has.ResourcesOfType<Invoice>()
                .AtUri("/invoice")
                .HandledBy<InvoiceHandler>()
                .AsXmlDataContract();
        }
    }
}

The configuration happens through a fluent interface. The ResourceSpace is the root object where you can define the resources in your application, what handles them and how they are represented. In this case this is going to be a fairly simple example. As it is a fluent interface it does seem to be fairly self explanatory.

The Invoice class is a simple POCO DTO that represents an invoice. POCO means Plain Old CLR Object and DTO is a Data Transfer Object. In this example the Invoice just looks like this:

public class Invoice
{
    public string Reference { get; set; }
}

The InvoiceHandler class is another POCO that happens to have methods on it that are picked up by the use of conventions. If you have a method named after an HTTP verb (like GET or POST) then OpenRasta will use it to handle that verb.

In this example we are just going to return a simple Invoice object. I don’t want to complicate the example with other things at the present, so it will, in fact, always return an invoice with the same Reference property value.

public class InvoiceHandler
{
    public Invoice Get()
    {
        return new Invoice
        {
            Reference = "123-456/ABC"
        };
    }
}

As the configuration specified that the XML Data Contract codec was to be used the invoice is rendered using that codec. The output looks like this:

<?xml version="1.0" encoding="utf-8"?>
<Invoice xmlns:i="http://www.w3.org/2001/XMLSchema-instance"          xmlns="http://schemas.datacontract.org/2004/07/MyFirstOpenRastaProject.Resources">
  <Reference>123-456/ABC
</Invoice>

Obviously at this stage it isn’t very useful. This is just a quick demonstration showing how quickly something can be set up. In coming parts I’ll be addressing other issues that so that more useful things can be done.

 

NOTE: This blog post is based on OpenRasta 2.0 Beta 2 (2.0.2069.364): [Download]

Technorati Tags:

SQL Injection Attacks and Tips on How to Prevent Them

I’m giving a talk in Dundee on the topic of SQL Injection Attacks. If you are interested in the subject then the registration link is at the bottom of the page.

Wednesday, 28th October 2009 at 19:00 – 21:00
Queen Margaret Building, Dundee University

The Talk

In light of some recent events, such as the man who was convicted of stealing 130 million credit card details through a SQL Injection attack, it is imperative that developers understand what a SQL Injection Attack is, how they are carried out, and most importantly, how to defend your code against attack.

In this talk I’ll demonstrate a SQL Injection Attack on an application in a controlled environment*. I’ll show you where the vulnerable code lies and what you can do to harden it.

Although this talk uses C# as the application language and Microsoft SQL Server 2008 as the database engine many of the concepts and prevention mechanisms will apply to any application that accesses a database through SQL.

* Demonstrating an attack on a real system without the owner’s consent is a breach of the 1990 Misuse of Computers Act, hence the controlled environment.

The Venue

We are meeting in the Queen Mother Building at Dundee University. After the meeting we normally retire to the the bar at Laing’s

The Agenda

18:45 Doors Open
19:00 Welcome
19:10 The Talk (Part 1)
19:55 Break
20:05 The Talk (Part 2)
20:45 Feedback & Prizes
21:00 Repair to the Pub

Registration

Space is limited, we would therefore ask that you sign up.

If you really must do dynamic SQL…

I may have mentioned in previous posts and articles about SQL Injection Attacks that dynamic SQL (building SQL commands by concatenating strings together) is a source of failure in the security of a data driven application. It becomes easy to inject malicious text in there to cause the system to return incorrect responses. Generally the solution is to use parameterised queries

However, there are times where you may have no choice. For example, if you want to dynamically reference tables or columns. You can’t do that as the table name or column name cannot be replaced with a parameter. You then have to use dynamic SQL and inject these into a SQL command.

The problem

It is possible for SQL Server to do that concatenation for you. For example:

CREATE PROCEDURE GetData
	@Id INT,
	@TableName sysname,
	@ColumnName sysname
AS
BEGIN
	SET NOCOUNT ON;

	DECLARE @sql nvarchar(max) =
		'SELECT ' + @ColumnName +
		' FROM ' + @TableName +
		' WHERE Id = '+cast(@Id as nvarchar(20));
	EXEC(@sql)
END
GO

This is a simple stored procedure that gets some data dynamically. However, even although everything is neatly parameterised it is no protection. All that has happened is that the location for vulnerability (i.e. the location of the construction of the SQL) has moved from the application into the database. The application is now parameterising everything, which is good. But there is more to consider than just that.

Validating the input

The next line of defence should be verifying that the table and column names passed are actually valid. In SQL Server you can query the INFORMATION_SCHEMA views to determine whether the column and tables exist.

If, for example, there is a table called MainTable in the database you can check it with a query like this:

SELECT * FROM INFORMATION_SCHEMA.TABLES
WHERE TABLE_NAME = 'MainTable'

And it will return:

INFORMATION_SCHEMA.TABLES

There is a similar view for checking columns. For example:

INFORMATION_SCHEMA.COLUMNS

As you can see, the INFORMATION_SCHEMA.COLUMNS view also contains sufficient detail on the table so that when we implement it we only have to make one check:

ALTER PROCEDURE GetData
	@Id INT,
	@TableName sysname,
	@ColumnName sysname
AS
BEGIN
    SET NOCOUNT ON;

    IF EXISTS (SELECT * FROM INFORMATION_SCHEMA.COLUMNS
               WHERE TABLE_NAME = @TableName AND COLUMN_NAME = @ColumnName)
    BEGIN
        DECLARE @sql nvarchar(max) =
            'SELECT ' + @ColumnName +
            ' FROM ' + @TableName +
            ' WHERE Id = '+cast(@Id as nvarchar(20));
        EXEC(@sql)
    END
END
GO

Formatting the input

The above is only part of the solution, it is perfectly possible for a table name to contain characters that mean it needs to be escaped. (e.g. a space character or the table may share a name with a SQL keyword). To escape a table or column name it is enclosed in square brackets, so a table name of My Table becomes [My Table] or a table called select becomes [select].

You can escape table and column names that wouldn’t ordinarily require escaping also. It makes no difference to them.

The code now becomes:

ALTER PROCEDURE GetData
	@Id INT,
	@TableName sysname,
	@ColumnName sysname
AS
BEGIN
    SET NOCOUNT ON;

    IF EXISTS (SELECT * FROM INFORMATION_SCHEMA.COLUMNS
               WHERE TABLE_NAME = @TableName AND COLUMN_NAME = @ColumnName)
    BEGIN
        DECLARE @sql nvarchar(max) =
            'SELECT [' + @ColumnName + '] ' +
            'FROM [' + @TableName + '] ' +
            'WHERE Id = '+cast(@Id as nvarchar(20));
        EXEC(@sql)
    END
END
GO

But that’s not quite the full story.

Really formatting the input

What if you have a table called Cra]zee Table? Okay – Why on earth would you have a table with such a stupid name? It happens, and it is a perfectly legitimate table name in SQL Server. People do weird stuff and you have to deal with it.

At the moment the current stored procedure will simply fall apart when presented with such input. The call to the stored procedure would look like this:

EXEC GetData 1, 'Cra]zee Table', 'MadStuff'

And it gets past the validation stage because it is a table in the system. The result is a message:

Msg 156, Level 15, State 1, Line 1
Incorrect syntax near the keyword 'Table'.

The SQL produced looks like this:

SELECT [MadStuff] FROM [Cra]zee Table] WHERE Id = 1

By this point is should be obvious why it failed. The SQL Parser interpreted the first closing square bracket as the terminator for the escaped section.

There are other special characters in SQL that require special consideration and you could write code to process them before adding it to the SQL string. In fact, I’ve seen many people do that. And more often than not they get it wrong.

The better way to deal with that sort of thing is to use a built in function in SQL Server called QUOTENAME. This will ensure the column or table name is properly escaped. The stored procedure we are now building now looks like this:

ALTER PROCEDURE GetData
	@Id INT,
	@TableName sysname,
	@ColumnName sysname
AS
BEGIN
    SET NOCOUNT ON;

    IF EXISTS (SELECT * FROM INFORMATION_SCHEMA.COLUMNS
               WHERE TABLE_NAME = @TableName AND COLUMN_NAME = @ColumnName)
    BEGIN
        DECLARE @sql nvarchar(max) =
            'SELECT ' + QUOTENAME(@ColumnName) +
            ' FROM ' + QUOTENAME(@TableName) +
            ' WHERE Id = '+cast(@Id as nvarchar(20));
        EXEC(@sql)
    END
END
GO

Things that can be parameterised

There is still something that can be done to this. The Id value is being injected in to the SQL string, yet it is something that can quite easily be parameterised.

The issue at the moment is that the SQL String is being executed by using the EXECUTE command. However, you cannot pass parameters into this sort of executed SQL. You need to use a stored procedure called sp_executesql. This allows parameters to be defined and passed into the dynamically created SQL.

The stored procedure now looks like this:

ALTER PROCEDURE GetData
	@Id INT,
	@TableName sysname,
	@ColumnName sysname
AS
BEGIN
    SET NOCOUNT ON;

    IF EXISTS (SELECT * FROM INFORMATION_SCHEMA.COLUMNS
               WHERE TABLE_NAME = @TableName AND COLUMN_NAME = @ColumnName)
    BEGIN
        DECLARE @sql nvarchar(max) =
            'SELECT ' + QUOTENAME(@ColumnName) +
            ' FROM ' + QUOTENAME(@TableName) +
            ' WHERE Id = @Identifier';
        EXEC sp_executesql @sql, N'@Identifier int',
                           @Identifier = @Id
    END
END
GO

This is not quite the end of the story. There are performance improvements that can be made when using sp_executesql. You can find out about these in the SQL Server books-online.

And finally…

If you must use dynamic SQL in stored procedures do take care to ensure that all the data is validated and cannot harm your database. This is an area in which I tread very carefully if I have no other choice.

Try and consider every conceivable input, especially inputs outside of the bounds of your application. Remember also, that defending your database is a multi-layered strategy. Even if you have the best firewalls and security procedures elsewhere in your system a determined hacker may find a way though your other defences and be communicating with the database in a way in which you didn’t anticipate. Assume that an attacker has got through your other defences, how do you provide the data services to your application(s) yet protect the database?

Tip of the Day #16: NaN (Not a Number)

The Issue

If you want to detect if a double (System.Double) or float (System.Single) is ?not a number? or NaN you cannot use something like this:

if (myDouble == double.NaN)
{
   /* do something */
}

It will always be false.

Sounds crazy? Try this:

double myDouble = double.NaN;
Console.WriteLine("myDouble == double.NaN : {0}", myDouble == double.NaN);

The result is:

myDouble == double.NaN : False

You can see that myDouble was explicitly set the value of double.NaN, yet in the next line it is returning false.

The Solution

If you want to test for a floating point value being Not a Number you to use IsNan() which is a static method on System.Double and System.Single. Here is the first example re-written to use the static method. It will now work correctly:

if (double.IsNan(myDouble) { /* do something */ }

If we re-write our other example:

double myDouble = double.NaN;
Console.WriteLine("double.IsNaN(myDouble) : {0}", double.IsNaN(myDouble));

We get the expected result too:

double.IsNaN(myDouble) : True

The Reason

According to Wikipedia: In computing, NaN, which stands for Not a Number, is a value or symbol that is usually produced as the result of an operation on invalid input operands, especially in floating-point calculations. For example, most floating-point units are unable to explicitly calculate the square root of negative numbers, and will instead indicate that the operation was invalid and return a NaN result. NaNs may also be used to represent missing values in computations.

It goes on to say: A NaN does not compare equal to any floating-point number or NaN, even if the latter has an identical representation. One can therefore test whether a variable has a NaN value by comparing it to itself, thus if x = x gives false then x is a NaN code.

This is why (double.NaN == double.NaN) always results in false. And it is also how the .NET framework detects the NaN value in the IsNan() method.

public static bool IsNaN(double d)
{
     return (d != d);
}

Interpreting promotional codes

The application I’m working on these days has a thing in it called a booking code. Now a booking code can be many things. It can tell you which discount code to use, which third party was being used to make the order, which business customer is making the order and so on. It is really just a convenience on the user interface so we didn’t have a large selection of text boxes asking for a discount code, merchant code or corporate customer ID, etc.

The application can determine what the code is based on which prefix has been used. “D” for a discount, “ME” for a merchant, “CC” for a corporate customer and so on.

So far all this seems fairly simple and straight forward. It is very simple to perform some conditional based on, for example,

string.StartsWith("ME")

However, it does mean that the code is soon cluttered up with lots of conditional statements testing the start of a string with magic values.

Of the two issues here the easiest to address is the magic values. It is easy enough to set up a series of const values representing the various prefixes. For example:

public const string MerchantPrefix = "ME";
public const string DiscountPrefix = "D";
public const string CorporateCustomerPrefix = "CC";

Then when a condition has to be met then the constant value can be used. This reduces the potential number of errors in the code because if the name of the constant is mistyped the compiler will catch it. If the magic value is mistyped then bugs can be introduced. Also, by using a constant value you can provide more meaningful names than the prefix alone can.

However, this is not the end of the story.

Because these prefixes are artificial there was also lots of code to strip off the prefix and check that the right number of characters was being stripped off, or have none stripped off (because in one case that type of code genuinely did always have that prefix in the back-end system). It seemed a lot easier to me to refactor the code and create a BookingCode class that encapsulated that functionality.

The new class takes in its constructor the code as the user would have typed it in. It has a property that exposes the type of booking code as a enum so it can be easily be used in switch statements. It also has a number of properties along the line of IsMerchantIdentifier, IsDiscountCode or IsCorporateCustomer for use where a single departure from the normal processing was required (i.e. an if statement).

Finally, the BookingCode class has a string property that exposes the actual code that the back end system needs. That way all the code that was stripping off the prefixes can be removed. The possibility of introducing intermittent errors is also reduced because there is now only one place where the code the user typed in is deconstructed into its component parts.

All in all this is a much more robust solution to the way the code used to work.

NOTE: The details in the example code above serve as an example only and do not represent the actual system in production.

Tip of the Day #15: Loop Performance

When you look at the code it will probably seem somewhat obvious, but it is interesting how the same thought process isn?t necessarily there when actually developing the code, especially when under the pressure of a looming deadline.

Take for example this snippet of code (from a fictitious hotel management system) that may have been run by a receptionist to print out the check in forms that customers of the hotel will fill in on their arrival.

List bookings = GetTodaysBookings();foreach (Booking booking in bookings)
{
    PrintService service = GetLocalPrintService();
    service.PrintCheckInForm(booking);
}

The method GetLocalPrintService() could be doing a multitude of things. It may simply be creating a new instance of the PrintService object, or it could be resolving a number of dependencies in order to set itself up to communicate with the local printer. But what ever it is doing, we don’t actually need a new instance of the service on each loop. The code will work just as well if we have just one instance that is re-used on each iteration of the loop.

That being the case, the creation of the PrintService can be moved outside the loop so it is created only once, thus removing unnecessary work from the loop. The new code then looks like this:

List bookings = GetTodaysBookings();
PrintService service = GetLocalPrintService();
foreach (Booking booking in bookings)
{
    service.PrintCheckInForm(booking);
}

As I said at the top, this is obvious. Isn’t it?

Tip of the Day #14: A Step to PCI Compliance

If you have a public facing website that accepts credit card payments from customers they you?ll be looking to become PCI compliant. This means you need to improve the security of your website to prevent attack and to prevent data being intercepted by third parties.

SSL 2.0 is now seen as weak and insecure, yet IIS will by default accept connections from older browsers that want to use this. It can be turned off, but it isn?t obvious how to do that. Here?s how to turn off SSL 2.0 on IIS or Microsoft Support has a reference on How to disable PCT 1.0, SSL 2.0, SSL 3.0 or TLS 1.0 in IIS (Internet Information Services).

While many PCI auditing companies will tell you if you are using SSL 2.0 or any other weak techniques, the quick test to ensure the server is not serving pages using SSL 2.0 is to change the Advanced Options in Internet Explorer to only support SSL 2.0.

Internet Options 1 (SSL)

After that I went to a secure page in the site and got the following error message:

Internet Explorer cannot display the webpage

Most likely causes:
  • You are not connected to the Internet.
  • The website is encountering problems.
  • There might be a typing error in the address.

What you can try:

Diagnose Connection Problems
More information

This problem can be caused by a variety of issues, including:

  • Internet connectivity has been lost.
  • The website is temporarily unavailable.
  • The Domain Name Server (DNS) is not reachable.
  • The Domain Name Server (DNS) does not have a listing for the website’s domain.
  • If this is an HTTPS (secure) address, click Tools, click Internet Options, click Advanced, and check to be sure the SSL and TLS protocols are enabled under the security section.

For offline users

You can still view subscribed feeds and some recently viewed webpages.
To view subscribed feeds

  1. Click the Favorites Center button , click Feeds, and then click the feed you want to view.

To view recently visited webpages (might not work on all pages)

  1. Click Tools , and then click Work Offline.
  2. Click the Favorites Center button , click History, and then click the page you want to view.

To ensure the site was working normally, I reset the settings to allow only support SSL 3.0 and TLS 1.0 and tried again.

Internet Options 2 (SSL)

This time I got the page I was expecting.

Note: You cannot use FireFox to perform this quick test as it does not support SSL 2.0.

Internet Options 3 (SSL/FF)