Writing CGI programs in Visual Basic

Extending the abilities of a web server is often done in interpreter languages like Perl or Python. If the server runs under Windows, a compiled Visual Basic program can be used alternatively, improving speed and security. Our first example is written for conventional HTTP servers like Apache or Microsoft IIS, using the STDIN and STDOUT mechanism. The second one is for the HTTP server of Shamrock NetMail, searching a text file for a phrase and displaying the result. Security considerations are discussed, too.

Apache: Handling STDIN and STDOUT

Most web servers use file handles for standard-in and standard-out for passing form data to a CGI program (Common Gateway Interface), and for getting the answer page from it. The sample program shows how to do this in Visual Basic. Note that this program does not contain a VB form, but a module with Main( ) instead. This reduces the overhead significantly since no window is required for this application.

The program can be called with something like
<FORM action="cgi-bin/cgitest.exe" method="POST">
to pass values from an HTML page using the POST method via STDIN, or
<FORM action="cgi-bin/cgitest.exe" method="GET">
for the GET method using a query string (like http://domain/cgi-bin/testcgi.exe?q=Test) which can be used if the resulting URL length is less then 1000 characters.

Our test program simply echoes the entered form values to an answer page. Since native Basic commands like Open and Close cannot be used for STDIN and STDOUT, a couple of Windows API functions must be declared instead. The server passes the request method (GET or POST) and, in case of POST, the input length as environment variables.

Option Explicit: DefLng A-Z

'HTML header and footer for answer page
Const HtmlHdr$ = "Content-Type: text/html" & vbCrLf & vbCrLf & _
 "<html><head>" & vbCrLf & "<title>CGI</title></head><body>" & vbCrLf
Const HtmlFtr$=vbCrLf & "</body></html>" & vbCrLf

'Windows API functions
Declare Function GetStdHandle Lib "kernel32" (ByVal h&)
Declare Function ReadFile Lib "kernel32" (ByVal hFile, ByVal _
 Buffer As Any, ByVal NumberOfBytes, BytesRead, ByVal Overlapped)
Declare Function WriteFile Lib "kernel32" (ByVal hFile, ByVal _
 Buffer As Any, ByVal NumberOfBytes, BytesWritten,ByVal Overlapped)

Sub Main()
 Dim e$, g$, r&, h&
 g$ = Environ$("REQUEST_METHOD")
 If g$ = "GET" Then
  e$ = Environ$("QUERY_STRING") 'Method is GET
  h = GetStdHandle(STD_INPUT_HANDLE) 'If POST, use STDIN
  e$ = String$(Val(Environ$("CONTENT_LENGTH")),0)
  ReadFile h, e$, Len(e$), r, 0
 End If
 g$=HtmlHdr & "<p>Method: " & g$ & "<br>Input: " & e$ & "</p>" & HtmlFtr
 h = GetStdHandle(STD_OUTPUT_HANDLE): WriteFile h, g$, Len(g$), r, 0
Exit Sub

All form parameters from the request page are typically passed url-encoded (delimited by &), and the text of the answer page must use HTML entities for special characters like <, >, & and ". Decode$( ) and HtmlEsc( ) in the next sample show how to achieve this. The only exception to the url-encoded format is when the form contains a file selection box for file uploads. In this case, the parameters are not escaped, and a boundary is defined in the first line which then delimits all following parameters and file contents, see the sample below.

'NMTEST.EXE: CGI sample for NetMail
Option Explicit: DefLng A-Z

Sub Main()
Dim i, j, k, lines, rdy As Boolean
Dim e$, f1$, f2$, fi$, search$,pair$
On Error GoTo fail
fi$ = "C:\TEST.TXT" '<--- YOUR FILE!
e$ = Command$: i = InStr(e$,""" """)
If i Then 'Split commandline
 f1$ = Mid$(e$, 2, i - 2)
 f2$ = Mid$(e$, i + 3, Len(e$)-i-3)
 'Analyze form data
 Open f2$ For Input As 1: j = 1
 Line Input #1, e$ 'Ignore MIME type
 Line Input #1, e$: Close 1 'Content
 Do: i = InStr(j, e$, "&")
  If i Then
   pair$=Mid$(e$, j, i - j): j = i+1
   pair$ = Mid$(e$, j)
  End If
  k = InStr(pair$, "=") 'name=value?
  If k Then
   If Left$(pair$,k-1)="Search" Then
   End If
  End If
 Loop Until i = 0
 'Create output file
 Open f1$ For Output As 1
 Print#1, "<h3>Results</h3><pre>"
 If LenB(search$) = 0 Then
  Print#1,"No search string entered"
 ElseIf LenB(Dir$(fi$)) = 0 Then
  Print #1, fi$ & " does not exist"
 Else:Open fi$ For Input Shared As 2
  While Not EOF(2) And Not rdy
   Line Input #2, e$
   If InStr(1, e$, search$, 1) Then
    HtmlEsc e$: Print #1, e$
    lines = lines + 1
    If lines >= 500 Then 'Max. lines
     Print #1, "...": rdy = True
    End If
   End If
  Wend: Close 2
  Print #1, "Found lines: " & lines
 End If
 Print #1,"</pre></body></html>"
 Close 1
End If
fail: 'Errors go here
End Sub

Private Function Decode$(g$)
'Decodes an URL-encoded string
Dim i, j, f$, e$
e$ = g$: Replace e$, "+", " ": j = 1
Do: i = InStr(j, e$, "%")
 If i Then
  f$ = f$ & Mid$(e$, j, i - j) & _
  Chr$(Val("&H" & Mid$(e$, i+1, 2)))
  j = i + 3
 Else: f$ = f$ & Mid$(e$, j)
 End If
Loop Until i = 0: Decode$ = f$
End Function

Private Sub HtmlEsc(g$)
'Replaces special char's by entities
Replace g$, "&", "&amp;"
Replace g$, "<", "&lt;"
Replace g$, ">", "&gt;"
 Replace g$, """", "&quot;"
End Sub

Private Sub Replace(e$, f$, g$)
'Replaces string f$ in e$ by g$
Dim i, j, k
j = 1: k = Len(g$) - Len(f$) + 1
Do: i = InStr(j, e$, f$)
 If i Then
  e$ = Left$(e$, i - 1) & g$ _
  & Mid$(e$, i + Len(f$)): j = i + k
 End If
Loop Until i = 0 Or j < 1
End Sub

The answer must contain a content-type as the very first line, followed by two CRLFs. The server will use this in the HTTP header it sends to the client browser. In most cases it will be text/html as above.

File-based I/O with NetMail

The HTTP server in the Mailer module of NetMail, primarily used for the webmail interface, also has a CGI interface (Mailer version 2.11 or higher). However, the implementation differs somewhat from Apache and other servers to make application programming even easier:

The sample program searches a predefined text file for a phrase entered in an HTML form which looks like this:

<form action="cgi-bin/nmtest.exe" method="POST">
<input type="text" name="Search">
<input type="submit" value="Find!"></form>

When a user enters "Basic" and clicks on the submit button in this form, a file is written by the NetMail server with a two-line content like this (a space was encoded as "+" in this sample!):


As in the first example above, no VB form is used since we do not need a window. Instead, the following code is a VB module with Main( ) as its entry point. Note that we do not need a single Windows API declaration here. - To optimize performance, it is useful to keep another small VB program in memory which uses the same (typically large) run-time DLLs so that they are not unloaded by Windows.

File upload

Though no explicit sample program for uploading files is given here, it must be mentioned that this requires a special "enctype" parameter in the HTML form tag:

<form action="cgi-bin/nmtest.exe" method="POST" enctype="multipart/form-data">
<input type="text" name="Name">
<input type="file" name="File">
<input type="submit" value="Upload"></form>

Now, when someone enters "Jim" as his name and uploads a short 1-line text file t.txt, the resulting temporary NetMail file looks like this:

multipart/form-data; boundary=d9jc
Content-Disposition: form-data; name="Name"

Content-Disposition: form-data; name="File"; filename="t.txt"
Content-Type: text/plain

This is the content of the uploaded file.

This looks quite similar to MIME-encoded e-mails with several attachments, separated by a random boundary string, but there is a big difference: The browser does not convert binary files using the BASE64 algorithm, they remain binary.

Security considerations

CGI programs are potential causes of security threats if they are not implemented carefully. One scenario which should be considered in all possible variations is that an attacker writes an HTML file on his local hard disk with the form "action" parameter pointing to your CGI program in the web.

For instance, if the CGI program does not use hardcoded filenames but gets a pathname from a form field of the requesting page, an attacker could access virtually any file on your server and even on network shares. The same is true for allowing shell commands, of course. Even if the path is hardcoded and only the filename is a form field, one must be aware that names like ../bin/somefile might be used to access files in other directories, so parsing names for ".." is a must.

When the CGI program calls another program such as sendmail, it must ensure that no redirection or pipe symbols like <, >, | can be passed to the commandline of this program, e.g. as a part of an address entered in a form field. It is best to allow only characters which are typically part of an e-mail address.

If a form is used to send e-mails, it must also be avoided that a spammer is able to use the CGI program as a mail relay, sending his unwanted marketing messages to third parties. The logical consequence is that an e-mail form must never contain the destination address in any of its fields.

The NetMail HTTP server has some built-in limitations regarding the cgi-bin folder for security reasons:

Entering "CGI security" into an Internet search engine will show many more aspects of potential security threats. Unfortunately, not all web developers are aware of them. For example, thousands of spam mails are relayed by insecure CGI scripts every day, and many servers are defaced through security holes.

04/2006 Shamrock Software GmbH