Overview

There are three things to remeber about web programming security that you have to bear in mind when designing your applications. These are, respectively, validation, validation and validation. The first thing a cracker will do to your site is to see where it breaks.

(note: Cracker is the correct use of the term here. Hackers build things, crackers break things
Most real hackers think that crackers are lazy, irresponsible and not very bright and object that being able to break security no more makes you a hacker that being able to hotwire cars makes you an automotive engineer
Eric Raymond
)

Common exploitative techniques

First Johnny Cracker will browse your site until he finds a script that accepts GET input in the format:
Code:
scriptname.ext?parameter=value
More experienced crackers will also be able to use the same sort of explotative techniques to attack scripts that accept POSTed variables, so don't assume that POSTing form data will deter a serious cracker (but it might exclude some of the more lame kiddies). For the examples in this tutorial I'm going to stick to basic GETs however.

Next our cracker will play with the URL to see what he can get to break. For example:
Code:
scriptname.ext?parameter=
If our script does not validate that the parameter has been passed then this will likley generate an error. If our code contains an SQL statement that looks like this:
Code:
set myRS = cn.execute("select * from pubs where parameter=" & request("parameter"))
then the cracker will get an error message containing the SQL statement and probably some infomation about the physical location of the script on the server. These are not good things to be giving out to Johnny Cracker.

TIP: In IIS and Apache it is possible to stop the server sending detailed error messages to the client (in IIS this is under application/debugging). Whenever you are not actively debugging your application you should ensure that this option is checked. Otherwise every error message is a bit more information for Johnny Cracker to work with.

Let's say our cracker is malicious - They now know that we have a table called pubs in our database. They can now rework the URL to include SQL stuffing like this:
Code:
scriptname.ext?parameter=1;delete * from pubs
Bye-bye pubs table. Our code above will assume that everything after the semi-colon is another SQL query and run it with the same permissions that were set up to run the first query. If those permissions were to the master database of an MSSQL database then the cracker now has access to all the stored procedures, including xp_cmdshell to run programs from the command line, xp_sendmail to mail himself the contents of your tables etc.

TIP:Never connect a web script to the master database of a MSSQL server. Never run your web page scripts from an administrative account.

Overcoming these techniques through validation

In both of these instances we could have stopped these attempts with proper validation. For example if we had nested our SQL statement in the following clause:
Code:
IF NOT (Len(request("parameter")>0 AND INSTR(LCASE(request("parameter")),"delete") =0) THEN
	...Whatever...
END IF
then our cracker would not have been able to perform either of the above 'exploits'.

Now re-writting these validation sequences for each of your scripts is a real waste of time and can really get quite tedious. Me personally I prefer to throw each of my validation sequences into a class that I can re-use time and again.

NTSA's Validation script

Don't try to copy and paste this script (not least because it's taken from one of my ActiveX controls and probably won't run as is in ASP). I am including it in this tutorial only as an example of what YOUR validation class might include - mine is tied into a number of sub classes that I'm not including here (debugging classes, INI file class,etc), but you can get the gist of what I'm doing. This class encapsulates all my validation routines and on completion allows an array of reported errors to be passed back to the calling script for display to the user.

You would call the routines in this class like this:
Code:
  Dim ers As Boolean
  Dim Er As Variant
  
  ers = False
  
  Validate.ClearErrors
  If Not Validate.isvalid("number", tInteger, "1") Then ers = True
  If Not Validate.isvalid("password", tPassword, "123456") Then ers = True
  If Not Validate.isvalid("Date", tDate, "28/3/75") Then ers = True
  If Not Validate.isvalid("Email", tEmail, "si@ntsa.org.uk") Then ers = True
  If Not Validate.isvalid("Required", tRequired, "sdgv") Then ers = True
  If Not Validate.isvalid("NoSpaces", tNoSpaces, "12345") Then ers = True
  If Not Validate.isvalid("Message", tNotHTML, "") Then ers = True
  If Not Validate.isvalid("SQL Statement", tNotSQL, "select * from sysobjects") Then ers = True
  If Not Validate.isvalid("Card Number", tLUHN, "4121111111119") Then ers = True
  
  Validate.BadWordsINI = "c:\badwords.ini"
  If Not Validate.isvalid("Message", tNotObscene, "Message") Then ers = True
  
  If ers = True Then
    Er = Validate.GetErrors
    For n = 0 To MyArrayCls.uboundc(Er)
      Debug.Print Er(n, 0) & Er(n, 1)
    Next
  End If
The Class file

Code:
Enum etType
  tInteger = 0
  tDate = 1
  tEMAIL = 2
  tNoSpaces = 3
  tRequired = 4
  tPassword = 5
  tNotObscene = 6
  tNotHTML = 7
  tNotSQL = 8
  tLUHN = 9
End Enum

Dim Errors()
'Default Property Values:
Const m_def_BadWordsINI = ""
'Property Variables:
Dim m_BadWordsINI As String

Private Sub UserControl_Initialize()
UserControl.Height = 250
UserControl.Width = 250
End Sub

Private Sub UserControl_Resize()
UserControl.Height = 250
UserControl.Width = 250
End Sub

Public Function IsValid(tName As String, 
  tType As etType, 
  tValue As String) As Boolean

  Tron.Trace "Validate", "Evaluating [" & tValue & "] as type " & _
    TypeDescription(CInt(tType))
  
  Select Case LCase(Trim(tType))
    Case tInteger
       If AllowedChars(tValue, "0123456789.") = True Then
        IsValid = True
      Else
        IsValid = False
        ReDimErrors tName, " is not a valid number."
      End If
    
    Case tDate
      If IsDate(tValue) Then
        IsValid = True
      Else
        IsValid = False
        ReDimErrors tName, " is not a valid date"
      End If
    
    Case tEMAIL
      If Strings.CountInString(tValue, "@") = 1 
        And Strings.CountInString(tValue, ".") > 0 
        And Strings.CountInString(tValue, " ") = 0 Then
        IsValid = True
      Else
        IsValid = False
        ReDimErrors tName, " is not a valid E-mail address"
      End If
    
    Case tNoSpaces
      If InStr(tValue, " ") = 0 Then
        IsValid = True
      Else
        IsValid = False
        ReDimErrors tName, " cannot contain spaces"
      End If
      
    Case tRequired
      If Len(tValue) > 0 And tValue <> "" Then
        IsValid = True
      Else
        IsValid = False
        ReDimErrors tName, " cannot be nothing"
    End If
    
    Case tPassword
        If Len(tValue) > 5 Then
      IsValid = True
      Else
        IsValid = False
        ReDimErrors tName, " must be at least 6 characters long"
      End If
  
  Case tNotObscene
    If Not IsObscene(tValue) Then
      IsValid = True
    Else
      ReDimErrors tName, " may contain obscenity"
    End If
    
  Case tNotHTML
      If Not ContainsHTML(tValue) Then
        IsValid = True
      Else
        IsValid = False
        ReDimErrors tName, " cannot contain HTML"
      End If
        
    Case tNotSQL
      If Not ContainsSQL(tValue) Then
        IsValid = True
      Else
        IsValid = False
        ReDimErrors tName, " cannot contain SQL statements"
      End If
      
    Case tLUHN
      If LUHNCheck(tValue) Then
        IsValid = True
      Else
        IsValid = False
        ReDimErrors tName, " is not a valid credit card number"
      End If
      
  End Select
  
  Select Case IsValid
    Case True
      Tron.Trace "Validate", tName & _
         " value [" & tValue & "] is a valid " & _
         TypeDescription(CInt(tType)) & " value."
    Case false
      Tron.Trace "Validate", tName & _
         " value [" & tValue & "] is NOT a valid " & _
         TypeDescription(CInt(tType)) & " value."
  End Select
    
End Function

Private Sub ReDimErrors(tName As String, tErrDesc As String)

  Dim Rec()
  
 ' Select Case MyArrayCls.UboundC(Errors)
 '   Case -1
 '     ReDim Errors(0, 1)
 ' End Select
      
  ReDim Rec(1)
  Rec(0) = tName
  Rec(1) = tErrDesc
  Errors = MyArrayCls.AppendRecord(Errors, Rec)
  
End Sub

Public Function ClearErrors()
  Dim nowt()
  Errors = nowt
End Function

Public Function GetErrors() As Variant
  GetErrors = Errors
End Function

Private Function AllowedChars(CheckString As String, 
  p_AllowedList As String) As Boolean

Dim l As Integer
AllowedChars = True

For l = 1 To Len(CheckString)
  If Not InStr(p_AllowedList, Mid(CheckString, l, 1)) > 0 Then
    AllowedChars = False
    Exit Function
  End If
Next

End Function

Private Function TypeDescription(TypeVal As Integer) As String

  Select Case TypeVal
    Case 0
      TypeDescription = "Integer"
    Case 1
      TypeDescription = "Date"
    Case 2
      TypeDescription = "Email"
    Case 3
      TypeDescription = "NoSpaces"
    Case 4
      TypeDescription = "Required"
    Case 5
      TypeDescription = "Password"
    Case 6
      TypeDescription = "NotObscene"
    Case 7
      TypeDescription = "NotHTML"
    Case 8
      TypeDescription = "NotSQL"
    Case 9
      TypeDescription = "LUHN"
  End Select

End Function
Public Property Get BadWordsINI() As String
  BadWordsINI = m_BadWordsINI
End Property

Public Property Let BadWordsINI(ByVal New_BadWordsINI As String)
  m_BadWordsINI = New_BadWordsINI
  PropertyChanged "BadWordsINI"
End Property

'Initialize Properties for User Control
Private Sub UserControl_InitProperties()
  m_BadWordsINI = m_def_BadWordsINI
End Sub

'Load property values from storage
Private Sub UserControl_ReadProperties(PropBag As PropertyBag)

  m_BadWordsINI = PropBag.ReadProperty("BadWordsINI", m_def_BadWordsINI)
End Sub

'Write property values to storage
Private Sub UserControl_WriteProperties(PropBag As PropertyBag)
  Call PropBag.WriteProperty("BadWordsINI", 
    m_BadWordsINI, 
    m_def_BadWordsINI)
End Sub

Private Function IsObscene(tValue As String) As Boolean

    Dim BWCount As Integer
    Dim NoSpaces As String
        
    IsObscene = False
    NoSpaces = Strings.JustLetters(tValue)
    
    Tron.Trace "Validate", "Checking message: '" & tValue & "'"
    
    If InStr(NoSpaces, "****") > 0 Then
      Tron.Trace "Validate", "Found Obscenity!"
      Tron.Trace "Validate", "Message contains word '****'."
      IsObscene = True
      Exit Function
    End If

    tValue = " " & tValue
    lenmesstext = Len(tValue)
    badwords = "rudeword1,rudeword2,etc"
 
    If Not Len(m_BadWordsINI) = 0 Then
      Tron.Trace "Validate", "Using Bad Words INI file."
      If Not INIFile.LoadINIFile(m_BadWordsINI) Then
        Tron.Trace "Validate", "Could not load the INI file: " & _
        m_BadWordsINI
        badword = Split(badwords, ",")
      Else
        badword = INIFile.GetKey("BadWords")
        Tron.Trace "Validate", "Loaded word list from INI File: " & _
        m_BadWordsINI
      End If
    Else
      badword = Split(badwords, ",")
    End If
    
    INIFile.CloseINI
    
    bwc1 = UBound(badword)
    
    badwords = ""
    bwc2 = -1
    
  
    Tron.Trace "Validate", "Pass 1 - Checking for all words..."
    ' Pass 1
    For BWCount = 0 To bwc1
      If InStr(NoSpaces, badword(BWCount)) > 0 Then
        Tron.Trace "Validate", "Found what may be: '" & _
          badword(BWCount) & "'"
        badwords = badwords & badword(BWCount) & ","
        bwc2 = bwc2 + 1
      End If
    Next
  
    If bwc2 > -1 Then
      'Pass 2 - Dynamic Array
      badwords = Left(badwords, Len(badwords) - 1)
      Tron.Trace "Validate", 
        "Pass 2 - Message may be obscene - Checking: " & badwords
      badword = Split(badwords, ",")
      For N = 1 To lenmesstext
        For BWCount = 0 To bwc2
  
          testbw = " " & badword(BWCount)
          NoSpaces = Mid(tValue, N, 1) & 
            Strings.JustLetters(Mid(tValue, N + 1, lenmesstext))
          
          'Trace( "NoSpaces:" & nospaces)
          'Trace( "Testing:" & mid(nospaces,1,len(testbw)))
          'Trace( "Against:" & testbw)
  
          If LCase(Mid(NoSpaces, 1, Len(testbw))) = LCase(testbw) Then
            Tron.Trace "Validate", "Found Obscenity!"
            Tron.Trace "Validate", "Message contains word '" & Trim(testbw) & "'."
            IsObscene = True
            Exit Function
          End If
          
        Next
      Next
    End If
End Function

Private Function ContainsHTML(tValue As String) As Boolean

  If InStr(tValue, "<") > 0 Then
    ContainsHTML = True
  Else
    ContainsHTML = False
  End If

End Function

Private Function ContainsSQL(tValue As String) As Boolean

  tValue = LCase(tValue)
  
  If InStr(tValue, "delete") > 0 Or InStr(tValue, "select") > 0 & _
        Or InStr(tValue, "insert") > 0 Or InStr(tValue, "update") > 0 & _
        Or InStr(tValue, "exec") > 0 Or InStr(tValue, "xp_") > 0 Then
    ContainsSQL = True
  Else
    ContainsSQL = False
  End If

End Function

Private Function LUHNCheck(CardNumber As String) As Boolean

Dim N, tot, x, chk, checkdigit

If Len(CardNumber) = 0 Then
    LUHNCheck = False
    Tron.Trace "Validate", 
      "Card Number [ " & CardNumber & " ] is not a number"
  Else
    
    tot = 0
    chk = Right(CardNumber, 1)
    CardNumber = Left(CardNumber, Len(CardNumber) - 1)

    For N = Len(CardNumber) To 1 Step -2
      x = CInt(Mid(CardNumber, N, 1)) * 2
      If Len(x) > 1 Then
        tot = tot + CInt(Mid(x, 1, 1))
        'debug mid(x,1,1)
        tot = tot + CInt(Mid(x, 2, 1))
        'debug mid(x,2,1)
      Else
        tot = tot + CInt(Mid(x, 1, 1))
        'debug mid(x,1,1)
      End If
    Next

    For N = Len(CardNumber) - 1 To 1 Step -2
      tot = tot + CInt(Mid(CardNumber, N, 1))
      'debug mid(CardNumber,n,1)
    Next

    'debug tot

    checkdigit = CInt(CStr(CInt(Left(CStr(tot), 1)) + 1) & "0") - tot

    Tron.Trace "Validate", 
      "LUHN Checkdigit should be " & checkdigit
    Tron.Trace "Validate", 
      "Actual LUHN Check Digit " & chk

    If CInt(chk) = CInt(checkdigit) Then
      LUHNCheck = True
    Else
      LUHNCheck = False
    End If

End If
End Function