Overview
Error Handling has always been crucial for an application in a number of ways. It may affect the execution state of the application, or expose sensitive information to a user. If the error handling is not strong, it may aid the attacker, as the errors returned may assist them in constructing correct attack factors.
An important part of secure application development is to prevent leakage of superfluous information to end user. Error messages, if not proper, may give an attacker great insight into the inner workings of an application.
What are Exceptions
Moving on to the definition, Exceptions are basically the unforeseen errors that happen in our programs. Most of the time, one can, and should, detect and handle application errors in the code. For example, validate user input data, check for null objects, verify the values returned from methods are what one expect, are all examples of good standard error handling that one should be taking care of all the time.
Handling the Anomalies
Tracing and handling of execution time errors is one of the most crucial tasks ahead of any programmer. But, before discussing the same, let's look at compile time errors, which are errors that occur during compilation of application. They may cause due to bad coding, misspelling of syntaxes, and so on.
On the other hand, runtime errors occur at the time the program executes and can't be corrected. A developer can, however, take preventive measures while coding the program. To do so, he should first identify these two aspects:
- Discover the parts of a program that are most likely to emit errors at execution time.
- Handle those errors according to the language conventions.
When an exception occurs the program flow for the executing method is interrupted. If the exception is not handled explicitly, the method exits and the exception is escalated to the calling function. This calling function has the opportunity to handle that error. The process continues until the exception is handled by the application or it reaches the Language's runtime system.
An unhandled exception that reaches the Language's runtime system causes the immediate, abnormal termination of the program. This can be a problem as the exception is reported to the end user in form of a message or dialog box containing standard information and technical details that may be misunderstood. During debugging this may be useful but in a production system it is generally considered unacceptable. It can also permit the user to attempt to continue to run a program that, due to errors, has become unstable.
A generic custom error page for most errors is recommended. This approach makes it more difficult for attackers to identify signatures of potentially successful attacks. There are methods which can circumvent systems with leading error handling practices which should be kept in mind; Attacks like SQL injection can be used to address such generic responses.
The other key area relating to error handling is the premise of "fail securely". Errors induced should not leave the application in an insecure state. Resources should be locked down and released, sessions terminated (if required), and calculations or business logic should be halted (depending on the type of error, of course).
"The purpose of reviewing the Error Handling code is to assure that the application fails safely under all possible error conditions, expected and unexpected. No sensitive information is presented to the user when an error occurs."
Exception handling in General
C# provides an elegant way to handle runtime errors with the help of the try, catch, and finally keywords. No matter we write a code for a small application or a business level application, the exceptions could be categorized into 3 basic levels,
Level 1. Exception caused by failure of user data validation.
Level 2. Exceptions caused by anomaly in business logic.
Level 3. Application level Exceptions, caused due to application crash, Incorrect/Unexpected output from database, improper hosting of application, Framework level bugs etc.
Exception Handling and .Net
Centralizing the Exception handling
Level 3 exceptions commonly need to be centralized at application level, so that when an exception having such behavior occurs, it is taken care of with immediate effects, In .Net this could be achieved by two methods,
- Handling the Exception in Global.asax.
- Handling the exception in web.config.
Since these are the exceptions thrown by Runtime Environment, the exact behaviour of this type of exception is hard to trace.
Right Approach
This content focuses more on technical feasibility and implementation of the same, we see here how the points discussed above are disguised in the form of code and learn how to implement these points practically in our application.
The Building Blocks
In .NET a System.Exception object exists. Mostly used child objects such as Application Exception and System Exception are used. It's not recommended that one throws or catches a System Exception as that is thrown by CLR automatically.
When an error occurs, the system or the currently executing application reports it by throwing an exception containing information about the error. Once exception thrown, it is handled by the application or by the default exception handler. This Exception object contains methods such as:
- StackTrace of Exception,
- Source of Exception,
- Message in Exception,
- InnerException of that object.
In .NET we need to look at the error handling strategy from the point of view of global error handling and handling of unexpected errors.
To avoid program to crash, the exceptions must be caught using a try-catch statement.
Consider the following live scenarios,
Let's have an aspx page with the following controls, a textbox, a button and a gridview,
When user inputs a student id in the textbox and submits the form by clicking submit button Populate Grid), the gridview gets populated with the details of the student as per student id.
Code to populate grid ,
ASPX
.CS :
Following are the points that need to be taken care of,
- The text box value must be an integer value. (Input Validation)
- The student id provided should be greater than 0 and less than 5. (Business Logic Validation)
These are the rules known to a developer, but what if end user uses the application? Let's sneak peek into such scenario,
Scenario 1: User inputs correct value and presses submit button,
Here we get the desired result :-).
Scenario 2: User inputs a string or junk character and presses submit button,
Results in,
As we can clearly see, whole of the code is exposed to end user which is ethically meaningless.
Instead one should display a meaningful message to the user, so that next time he inputs correct info.
Therefore we need to wrap our code with a try, catch block.
And following piece of code in aspx page,
<div>
<asp:Label runat="server" ID="lblErrorDisplay" ForeColor="Red" FontBold="true" Visible="false" Font-Size="Medium" ></asp:Label>
</div>
Now when user inputs value other than integer, he is shown a meaningfull message as,
Our application should not scare the end user with displaying yellow pages and complex codes but it should be informative and end-user friendly.
Scenario 3: User inputs an integer but violates business logic by entering 5 or integer greater than 5,
Our case: No message is shown to the user, the page simply refreshes itself,
Ideal case: User should be shown a message that "no user exists with the specified id, please enter another id between 1 to 5", after getting this message user will no longer sit idle thinking about what is wrong with the web site, he will take some other fruitfull action.
To overcome such issue we can again decorate our code as,
protected void btnSubmit_Click(object sender, EventArgs e)
{
try
{
string textValue = txtId.Text;
int id;
if (!Int32.TryParse(textValue, out id))
{
throw new ApplicationException("Please provide correct student id.");
}
if (id <= 0 || id >= 5)
{
throw new ApplicationException("No user exists with the specified id, please enter another id between 1 to 5");
}
string connectionString = ConfigurationManager.ConnectionStrings["Connection"].ConnectionString;
SqlConnection sqlConnection = new SqlConnection(connectionString);
SqlCommand sqlCommand = new SqlCommand("Select StudentID,Fnamn,Enamn,Email,Login,word from egStudent where StudentId=" +
id.ToString(), sqlConnection);
DataSet dataSet = new DataSet();
SqlDataAdapter sqlDataAdapter = new SqlDataAdapter(sqlCommand);
sqlConnection.Open();
sqlDataAdapter.Fill(dataSet);
grdStudentView.DataSource = dataSet.Tables[0];
grdStudentView.DataBind();
sqlConnection.Close();
sqlDataAdapter.Dispose();
}
catch (ApplicationException exception)
{
lblErrorDisplay.Visible = true;
lblErrorDisplay.Text = exception.Message;
}
}
Therefore the output,
The Exceptions discussed above were the Level 1 and Level 2 type exceptions.
Next comes Level 3.For level 3 type exceptions a special setup needs to be established, so that the exceptions could be handled as desired, simple playing with try catch blocks sometimes does not work with these kind of exceptions,
Scenario 4: Developer mistakenly writes a wrong query in select statement, suppose he makes a typo and changes table name to egstudents (correct : egstudent), let's see what happens,
protected void btnSubmit_Click(object sender, EventArgs e)
{
try
{
string textValue = txtId.Text;
int id;
if (!Int32.TryParse(textValue, out id))
{
throw new ApplicationException("Please provide correct student id.");
}
if (id <= 0 || id >= 5)
{
throw new ApplicationException("No user exists with the specified id, please enter another id between 1 to 5");
}
string connectionString = ConfigurationManager.ConnectionStrings["Connection"].ConnectionString;
SqlConnection sqlConnection = new SqlConnection(connectionString);
SqlCommand sqlCommand = new SqlCommand("Select StudentID,Fnamn,Enamn,Email,Login,word from egStudents where StudentId=" +
id.ToString(), sqlConnection);
DataSet dataSet = new DataSet();
SqlDataAdapter sqlDataAdapter = new SqlDataAdapter(sqlCommand);
sqlConnection.Open();
sqlDataAdapter.Fill(dataSet);
grdStudentView.DataSource = dataSet.Tables[0];
grdStudentView.DataBind();
sqlConnection.Close();
sqlDataAdapter.Dispose();
}
catch (ApplicationException exception)
{
lblErrorDisplay.Visible = true;
lblErrorDisplay.Text = exception.Message;
}
}
And so, the output
In this special case, once the site is up, It becomes hard for developer too to trace the code, Moreover end user again is exposed to the unwanted yellow page.
Cure : These kind of Level 3 exceptions need to be centralized as discussed before in the article), We can write the piece of code in Global.asax on Application_Error event (handles application level errors), one can write the code to log the specific error and show end user a custom error page, to overcome panic situation.
Step 1. Add code to Global.asax,
void Application_Error(object sender, EventArgs e)
{
Exception exception = Server.GetLastError();
LoggError.Write(exception);
}
Step 2. Create a Custom Error Page, and define its path in web.config with a key value pair in appsettings,
ErrorPage.aspx
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.wHYPERLINK "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"3HYPERLINK "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd".org/TR/xhtmlHYPERLINK "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"1HYPERLINK "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"/DTD/xhtmlHYPERLINK "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"1HYPERLINK "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<title></title>
</head>
<body>
<center>
<div><img src="ErrorPage1.jpg" alt="Error Page" /></div>
</center>
</body>
</html>
Page Path
<appSettings>
<add key="ErrorLog" value="~/ErrorLog.txt" />
</appSettings>
Step 3. Enable Custom error mode in web.config inside system.web node with path to your error page as default redirect,
<customErrors mode="On" defaultRedirect="Error/ErrorPage.htm">
</customErrors>
Step 4. Add a class to your appCode folder, that logs the error into a text file.
using System;
using System.Configuration;
using System.IO;
using System.Web;
/// <summary>
/// The Class Writes Exception and Error information into a log file named ErrorLog.txt.
/// </summary>
public class LoggError
{
/// <summary>
/// Writes error occured in log file,if log file does not exist,it creates the file first.
/// </summary>
/// <param name="exception">Exception</param>
public static void Write(Exception exception)
{
string logFile = String.Empty;
StreamWriter logWriter;
try
{
logFile = HttpContext.Current.Server.MapPath(ConfigurationManager.AppSettings["ErrorLog"].ToString());
if (File.Exists(logFile))
logWriter = File.AppendText(logFile);
else
logWriter = File.CreateText(logFile);
logWriter.WriteLine("=>" + DateTime.Now + " " + " An Error occured : " +
exception.StackTrace + " Message : " + exception.Message + "\n\n");
logWriter.Close();
throw exception;
}
catch (Exception e)
{
throw e;
}
finally
{
throw exception;
}
}
}
Finally, when such error occurs, user is displayed an error page as below,
And for the sake of debugging and development, Actual error is logged in a text file (in my case located at root) as,
=>11/21/2011 7:42:02 PM An Error occured : at System.Web.UI.Page.HandleError(Exception e)
at System.Web.UI.Page.ProcessRequestMain(Boolean includeStagesBeforeAsyncPoint, Boolean includeStagesAfterAsyncPoint)
at System.Web.UI.Page.ProcessRequest(Boolean includeStagesBeforeAsyncPoint, Boolean includeStagesAfterAsyncPoint)
at System.Web.UI.Page.ProcessRequest()
at System.Web.UI.Page.ProcessRequestWithNoAssert(HttpContext context)
at System.Web.UI.Page.ProcessRequest(HttpContext context)
at ASP.default_aspx.ProcessRequest(HttpContext context) in c:\Users\akhil.mittal\AppData\Local\Temp\Temporary ASP.NET Files\exceptionhandling\327c9c5b\2858bbb8\App_Web_iexkgkvc.2.cs:line 0
at System.Web.HttpApplication.CallHandlerExecutionStep.System.Web.HttpApplication.IExecutionStep.Execute()
at System.Web.HttpApplication.ExecuteStep(IExecutionStep step, Boolean& completedSynchronously) Message : System.Data.SqlClient.SqlException (0x80131904): Invalid object name 'egStudents'.
at _Default.btnSubmit_Click(Object sender, EventArgs e) in d:\Akhil Mittal\Blogs\ExceptionHandling\Default.aspx.cs:line 56
at System.Web.UI.WebControls.Button.OnClick(EventArgs e)
at System.Web.UI.WebControls.Button.RaisePostBackEvent(String eventArgument)
at System.Web.UI.WebControls.Button.System.Web.UI.IPostBackEventHandler.RaisePostBackEvent(String eventArgument)
at System.Web.UI.Page.RaisePostBackEvent(IPostBackEventHandler sourceControl, String eventArgument)
at System.Web.UI.Page.RaisePostBackEvent(NameValueCollection postData)
at System.Web.UI.Page.ProcessRequestMain(Boolean includeStagesBeforeAsyncPoint, Boolean includeStagesAfterAsyncPoint)
The log gives full information about the type and source of exception.
There are many other ways to handle Level 3 Exceptions which we'll discuss in next coming articles, but to hit the floor running, this one is the beginning.
And last but not the least, the Finally block,
Finally block
As the last clause in the try-catch statement a finally block can also be added. This block is used to clean up all the resources allocated in the try block and will always execute whether there is an exception or not. In the above scenarios for example, we can make use of this block to free sql connection as ,
sqlConnection.Close();
sqlDataAdapter.Dispose();
And thus the final code,
protected void btnSubmit_Click(object sender, EventArgs e)
{
string textValue = txtId.Text;
int id;
string connectionString = ConfigurationManager.ConnectionStrings["Connection"].ConnectionString;
SqlConnection sqlConnection = new SqlConnection(connectionString);
SqlDataAdapter sqlDataAdapter = new SqlDataAdapter();
DataSet dataSet = new DataSet();
try
{
if (!Int32.TryParse(textValue, out id))
throw new ApplicationException("Please provide correct student id.");
if (id <= 0 || id >= 5)
throw new ApplicationException("No user exists with the specified id, please enter another id between 1 to 5");
SqlCommand sqlCommand = new SqlCommand("Select StudentID,Fnamn,Enamn,Email,Login,word from egStudents where StudentId=" +
id.ToString(), sqlConnection);
sqlDataAdapter = new SqlDataAdapter(sqlCommand);
sqlConnection.Open();
sqlDataAdapter.Fill(dataSet);
grdStudentView.DataSource = dataSet.Tables[0];
grdStudentView.DataBind();
}
catch (ApplicationException exception)
{
lblErrorDisplay.Visible = true;
lblErrorDisplay.Text = exception.Message;
}
catch (Exception exception)
{
throw exception;
}
finally
{
sqlConnection.Close();
sqlDataAdapter.Dispose();
}
}
Read more:
Other Series
My other series of articles:
For more informative articles visit my Blog.