As an ASP programmer, I am always writing code that accesses databases. Many applications, such as Site Server, Commerce Server, SharePoint, and Content Management Server provide their own API that helps an ASP programmer tie into this data in a secure and efficient way. That's nice if you have access to these remarkably expensive platforms, but what about the rest of us? Well, you could roll up your sleeves and just whip off a couple COM objects; however unless you are a crewmember of the starship Voyager, such miracles are unlikely.
Usually, what it comes down to is something more like this:
Set ADOConn = Server.CreateObject ("ADODB.Connection")
ADOConn.Open "myDataSource", "sa", "ItsASecret"
We need less than a second glance to see why this is bad. Any hacker who manages to view the ASP code will now have full access to your database server as well.
You've probably whipped up something like this while developing a new database driven web application. If you're anything like me, you don't want to be bogged down in supporting code and procedures. You want to get connected and begin writing feature code, the stuff that actually does something. "First things first." we tell ourselves; we'll go back and secure the application after we get it working.
If you have a good memory, and if you are not seriously overworked, you might actually come back and do this. When you finally do return to secure your code, there are a couple of things that can typically be done to help secure passwords like this one. First, we don't use the sa, or system administrator, account; that much is obvious. Secondly, we typically define database connections and the strings that support them in global.asa or an include file. But is this really enough?
Using an account other than sa is very important. After all, if a hacker were to acquire the connection name and password, you would want to limit the amount of access they would have to other databases that might be supporting other services on your site or even other customers. However, I say that this is insufficient. Your web application is going to want read and write access to the database in order to perform its duties. So, if a hacker has gotten this far, they've already got enough information to sabotage your site, place false orders, or download your entire user list.
Moving your connection strings to global.asa may seem like a good idea at first, but consider the fact that most hackers are very likely to look here first for critical application information. There are known loopholes that allow hackers access to global.asa, just as there are security issues that allow them to view your ASP code. Include files aren't much better, since hackers can usually view these just as any other ASP file on your site.
Don't think you've done enough to prevent this either. I am generally very thorough about security, but I was very alarmed one day when I received an anonymous e-mail from someone handing me a scrap of code from my site that contained the Administrator password for my entire domain! Fortunately for me, it was a password I had long since changed. (One wonders how my code functioned without it, though.)
This just illustrates my point; even if you are diligent now,
there's no guarantee that your server has always been
protected, particularly if you are sharing it with
others.
SQL databases are not the only thing that is susceptible to this kind of attack either. The administrator account I mentioned above was being used to access an LDAP directory. Many applications and frameworks that tie into ASP will require secured access. This is to prevent anonymous web users from accessing the API directly. But in so doing, they also expose us to the serious threat of compromising our security credentials. These can be SQL Server or other database accounts, LDAP directory accounts, or even privileged Windows user accounts. Literally, anything that needs this kind of protection can be at risk in this way.
So, what's a responsible programmer to do? Robert Howard, author of Site Server 3.0 Personalization and Membership (available from Wrox Press) recommends storing this critical information in the registry. There's only one problem. While Site Server and other high-end systems built on ASP often include a means of accessing the registry, Microsoft has, some would say thoughtfully, not included a standardized means of manipulating the registry from ASP. To his credit, Robert also briefly mentions the alternative we will illustrate today, even calling it preferable to using the registry. That alternative is to store our access codes in the IIS metabase.
Did I say preferable? Yes. In fact, the metabase is where IIS stores the usernames and passwords it uses to support itself and ASP. Unlike the registry, it not only includes a means of securing this content, but also a means for hiding passwords from casual observation. And--here's the great news--it comes built into IIS from version four onward.
Enter The Metabase
To do what I am suggesting, you are going to need some handy tools. One of these is the Metabase Editor, or MetaEdit for short. This tool is generously provided by Microsoft, and comes included in the IIS Resource Kit. You can also download it from Microsoft at http://support.microsoft.com/support/kb/articles/Q232/0/68.ASP.
Do yourself a favor and read the knowledge base
article if you haven't already. As the name implies, MetaEdit functions
with the metabase much like our old friend RegEdit did with the
registry. It also shares the same caveat, that you can do a considerable
amount of damage with it. Before you face that risk, back up your
metabase from the IIS management console, preferably several times. It
is extremely important to do this when you are writing code that
manipulates the metabase itself, because you will want to be able to
undo any potentially bad changes it makes.
Once you have downloaded and installed MetaEdit on your web server, open it and take a look around. You'll see that the metabase has a tree structure, very similar to the registry, or even Active Directory. In fact, like Active Directory (or any LDAP database for that matter) the metabase has a schema. The schema defines all the data types that can be defined within the metabase, in which containers they are valid, and other vital information.
So, this is where we'll begin. You need to define
data types that will store the username, password, and connection string
for our database. If you were connecting to LDAP or Active Directory,
you'd also need to create data types for these connections. There are
three paths in which your new data type will be defined. These are each
listed under the /Schema/Properties path, and are
Defaults, Names, and
Types. If you take a direct look at the values under
these paths, you can see that they are almost impossible to understand,
because much of the information is stored in binary. Fortunately, you
can extend the schema via the ADSI, or Active Directory Services
Interface, a COM object API that allows us to interact with the
metabase, as well as other directory structures. Through ADSI, we can
use VBScript or ASP to bind to the metabase and define our values.
Older versions of Windows NT 4.0 may not have the ADSI
installed. If this is the case on your server, you can download it from
Microsoft from the following URL:
We want to create three data types, which I have chosen to call ODBCDataSource, ODBCUserName, and ODBCPassword. The data stored in these values will be used to replace the text strings in that awful ADODB command at the beginning of this article. If we wanted to use DSN-less connections, you can extend this list further to include a server and database name as well. You can do the same kind of thing to add other types of connection information for WinNT, Active Directory, LDAP, or whatever you like.
What you don't want to do is take forever to get this part done.
After all, we're not even at the useful bits yet.
So, I've included a VBScript file called MetaSchema.vbs that you
can use to extend the metabase schema, so that it includes these
data types. Simply put the script on the desired server, open
our command prompt, navigate to it, and then type its name
to execute it. You'll need to run it using an account with
Administrator level access.
Our sample script does four things.
First it creates a class for the new data types.
I chose to call this DataAccessMethods.
Next, it creates the three data types we described,
then adds the data types to the class.
Finally, it creates a class for the container that will hold
each of our DataAccessMethods instances,
called DataAccessStorage.
In this example, all the data types are strings with default
settings for inheritance and security. Also, be aware that
the error detection is very rudimentary. If the script
detects an error, it will simply stop working. In many
cases it will skip the remaining code without even reporting
the error. As an advanced exercise you can add these features
later. However, for the purpose of illustrating our point,
this script will run fine as it is.
Now, here's a little about what is going on in this script.
If you've done programming using ADSI before, this code will
seem very basic to you. You may have been exposed to this
through Windows 2000 or Microsoft Site Server. Regardless
of whether you are familiar with ADSI or not, this code
should be reasonably self-explanatory, and you should be
able to familiarize yourself with the syntax by comparing
the path names you see in the code to the paths visible
when using MetaEdit.
The first thing it does after defining some constants
is bind to the IIS metabase schema.
' Bind to the Schema container object.
Set SchemaObj = GetObject ("IIS://" & MachineName & "/Schema")
This is done by using the machine name, in this case "localhost", to create the metabase path.
This path is then passed to the GetObject function,
which is part of the ADSI component model.
The remainder of the script uses the schema object to perform various functions.
Next, the script calls CreateClass, passing the
name of the new class, "DataAccessMethods".
CreateClass is a simple function, which attempts to create the
new class and returns TRUE if it succeeds.
The functional part of the code is:
Set NewClassObj = SchemaObj.Create ("Class", ClassName)
NewClassObj.SetInfo
If the class was created successfully, then the script will
create the properties themselves, adding each one to the class
only if it has also been successfully created.
In each case, the script will call CreateProperty
and AddToClass for each new property.
It does this through a subroutine called
CreateProperty_Plus_AddToClass,
the purpose of which is basically to save typing and
prevent potential spelling errors that would bomb the script.
The script also adds two predefined properties to the
DataAccessMethods property.
The first is KeyType, which the metabase uses
to determine the class of a key within the metabase;
generally speaking, all classes make use of this key.
The second is AdminACL, which determines
the security permissions that will be applied to a given
instance of the class.
Finally, the script creates class DataAccessStorage,
a class specially created to hold the key folder that contains
each of our DataAccessMethod keys.
The only properties of this class are KeyType
and AdminACL, which will allow us to set the
security permissions for the root container.
Here is the condensed code from CreateProperty.
Set NewPropertyObj = SchemaObj.Create ("Property", PropertyName)
If Trim(Syntax) = "" Then Syntax = "string" ' default is String
NewPropertyObj.Syntax = Syntax ' Set the syntax; must do pre-save
NewPropertyObj.SetInfo ' save to the metabase
NewPropertyObj.Inherit = True ' Set attributes by inheritance
NewPropertyObj.SetInfo ' save to the metabase
First, CreateProperty defines
NewPropertyObj, the new property object,
by calling the Create method of
SchemaObj, which we defined earlier.
At this point, nothing has changed in the metabase.
Before the new property can be saved, its syntax must be defined.
While there are many syntax types, "string" is
sufficient for our purposes here, and so we use it as the
default syntax. After setting the syntax, the script then
performs a SetInfo on the object.
This stores the item in the metabase.
After the property has been saved once, changes can be made
to its other settings, such as inheritance.
Don't forget to use SetInfo again to save
any changes you make at this point.
Now, let's move on to the code in AddToClass.
Set NewClassObj = GetObject ("IIS://" & MachineName _
& "/Schema/" & ClassName) 'Get the class object
'Get the optional properties list
OptPropList = NewClassObj.OptionalProperties
cnt = UBound(OptProplist)
'Add the new property to the array
ReDim Preserve OptPropList(cnt+1)
OptPropList(cnt+1) = PropertyName
'Write the values to the metabase
NewClassObj.OptionalProperties = OptPropList
NewClassObj.SetInfo
As before, GetObject will retrieve the class
object we want to add our properties to. Next,
OptPropList is set with the value of the class
object's OptionalProperties property.
All this talk of properties, property lists, and optional
properties has probably got your tongue tied in knots.
Just think of OptionalProperties as an array that contains
the list of properties that are part of the class, which
happens to be a property of another object altogether.
"I love properties! I'll have the properties,
properties, properties, properties, invoked methods,
and properties!"
(I refuse to take credit for this odd naming convention;
you can blame the nice folks at Microsoft for it, but it
certainly gives us an interesting insight into why they
call it a meta-base.)
Now that we've got that little confusion out of the way,
the next step is to set cnt to the upper bound of the
OptPropList array.
We do this so that we can extend the array by one in the
following code.
ReDim Preserve adds an additional element
to the array, leaving existing values intact.
This gives us just enough room to add the new property
name to the list. Once that's done, the script reassigns
OptPropList to the OptionalProperties
property. (Here we go again!) And, finally, there is one more call
to SetInfo to save the whole sordid mess to the metabase.
If that didn't confuse you as much as it did me the first time around,
you can take a look at RemoveFromClass, another function
I included that didn't get used here. There is also a wealth of
information about ADSI and the IIS metabase on the
Microsoft Developer's Network web site.
To learn more about extending the IIS Schema,
visit the Microsoft web site at:
The result of the preceding section and script is that
the IIS metabase has now been successfully prepared
to store the special data types that will be used to hold
the connection string, user name, and password for the database.
Next, we'll write those values, first using MetaEdit,
then using ASP.
Storing Values in the Metabase
Now let's have some fun. Open MetaEdit. If you left it open
while you ran MetaSchema.vbs, then close it and re-open it
to make sure it reflects the changes the script made
to the schema.
Once you have the Metabase Editor console open,
expand the Schema key, and then expand
Classes. You should see a key folder
named DataAccessMethods.
(Mine appeared at the top of the list.)
There's nothing of interest in it for us at the moment,
but it's important to note (and verify) that it is there.
Now, expand the /Schema/Properties/Names key.
If you sort the list in the right hand pane by Id,
you should see three items in the 13000 range that correspond
to the three values we defined using the MetaSchema.vbs script.
Each will start with "ODBC". The actual ID values may
vary depending on your server configuration.
Once you've verified that the new property types exist,
minimize the Schema key and expand the
LM key.
LM stands for "local machine",
by the way. Under this key you will see all kinds of keys
for different kinds of services running under IIS on the server.
You will want to create a new key to use for credential storage.
Create a new key by right clicking LM and
selecting New | Key. I chose to name my
key ASP101, but you can call it whatever
you like, as long as you also change any code that references
this path.
Create a new string value in the ASP101 key folder.
In the Id dropdown, choose KeyType,
and type "DataAccessStorage" in the data field.
This will help ADSI figure out what properties and objects are
supported by these containers, including the
AdminACL object, which will be essential,
as we'll see soon enough.
Next, we need to separate the credentials for this particular
web application from any others that we might potentially need
for other programs. To do this, create another key under
ASP101. I will call mine TestCred.
If you want, you can use a friendlier name like an IIS site name
or DNS name. Remember that the code in the upcoming samples is
based on our naming convention; you'll have to modify it to reflect
whatever names you decide to use. You'll need to create a new key
for each new set of credentials you require; expect at least one per
web site, but possibly more if your site has multiple levels of
access.
Now open the TestCred key. Add a new value by
right clicking it and selecting New | String.
A dialog box will open. In the Id field, use
the dropdown to find the ODBCDataSource
item and select it. Change the User Type
option to ASP App; this will relax security
a little so that your web application can read the data.
Check the inherit checkbox, if it is not already
selected. Finally, type your data source name into the
Data field and click OK.
Perform the same operations for ODBCUserName
and ODBCPassword. As an extra step, when you
create your ODBCPassword property, check the
box marked Secure. This will prevent people
from casually browsing with MetaEdit to
determine the database password. You will get a warning
about this step, telling you that it cannot be undone;
once a data property has been secured, it can not be
unsecured using MetaEdit.
We're almost done now. Create another string value in the
TestCred key and choose KeyType
for the Id. Put "DataAccessMethods"
in the data field and click OK. You don't need
to create the AdminACL keys for either
ASP101 or TestCred; we'll
explain why when we talk about metabase security.
So, now there are values stored for the TestCred
applications DSN, user name, and password. This is everything we
need to access the database from ASP, and maybe even more than
we needed to secure in this way. That's fine for one application,
but how can we automate the setting of these values, so that we
can administer multiple sites easily? The answer is to
use the same ADSI functions to read and write to this part of
the metabase as we'd use to access the schema.
If you want to write ASP scripts that set or change the values
stored in the metabase, you will need to force the user to
authenticate for those scripts. The user will need to log in
to an account belonging to the server's Administrators group.
Otherwise your scripts will fail in a profound way. In fact,
this is going to be a problem for our scripts when we read
the settings, as we'll explain in the next section.
Security and the Metabase
Before you can use ASP code to write to the database, or even read
from it, you need to consider metabase security and how it will
affect attempts to access this data. By default, only server
administrators can access the metabase information. Even
MetaEdit doesn't provide a means of changing the access control
lists within the metabase. Microsoft provides a sample showing
how to change security ACLs using VBScript though, and you can
download it here:
However, the error checking within the script is not very good,
and it has some bugs, too. Because of this, I have included a
revised version of the script, MetaACL.vbs that fixes a few
shortcomings and provides better error checking.
If you want you can grab parts of this code and append them
to MetaSchema.vbs to create one script that will set your
schema and also set security for you. In my opinion,
it is a better idea, to create a separate automated script
for this purpose, because you will only need to configure
the schema once, but you may reuse this security code to
help you configure many, many instances of our
DataAccessMethods class.
To change the permissions to suit our purposes, Everyone
must be granted the ability to Read entries, and
Enumerate objects. In order to do this, you'll have
to remove the existing ACL for Everyone
first. From the command line, go to the directory in which
you have placed the revised MetaACL.vbs script, then
type the following, hitting enter after each command:
METAACL "IIS://localhost/ASP101" Everyone -d
METAACL "IIS://localhost/ASP101" Everyone RE
METAACL "IIS://localhost/ASP101/CredTest" Everyone -d
METAACL "IIS://localhost/ASP101/CredTest" Everyone RE
This will remove Everyone from each part of
the tree, and recreate it with the correct permissions.
The second set of commands is important because, in this
example, we have not set AdminACL to
inherit settings from its parent. This is an improvement
that can certainly be made later.
True, these settings won't make your keys hack-proof. But
storing credentials here will help obfuscate your data,
so that it won't fall victim to some script-kiddy who
just happens to have learned to exploit CodeView.asp.
In time, you can do more to secure the metabase even further,
enhancing its ability to protect your data.
For example, you could easily make this part of an ASP based
web administration script that could both create the necessary
keys and properties, and set their security. I think it's a
fine idea, but I only have so much space in this article.
Also, it's more important for you to understand what is going
on in the metabase regarding creation of data and setting
security. Trust me, this will help you later when you do
begin writing code, and if you do that, then I've done my
job here.
Reading The Credentials from the MetaBase
Now that all this preparatory work is done, we arrive at our
anticlimactic ending. In fact you're probably going to wonder
how something so difficult to set up and configure could
possibly be this easy to use. Let's take a look at the code
for MetaRead.asp:
There is a lot of code there, but what it does is very simple.
First, it defines the names for the machine, root key, and
specific data access key that will be uses for this web
application. It uses the same names we defined when we
created keys earlier. If you created different keys,
remember to check your code here.
If this fails, we generate an error report and set the
Boolean MetaSuccess to FALSE.
Otherwise, we set MetaSuccess to TRUE.
Once we're past our trap, we turn errors back on.
If Err.number <> 0 Then
Response.Write "<P>ERROR Reading data access " _
& "credentials from metabase<BR/>"
Response.Write Err.Number & ": " & Err.Description
MetaSuccess = FALSE
Else
MetaSuccess = TRUE
End If
On Error Goto 0
At this point we take a brief moment to indulge in a little
optional code. The next section just displays the values
we've read in HTML. Obviously, this serves no purpose but
to help inflate our egos a bit after a hard day of wrestling
the metabase beast, and it should be removed from any real
applications for this code.
The last section of the code checks for success by testing
MetaSuccess. If TRUE, it
will create an ADODB Connection object,
then call the Open method to connect itself
to an ODBC data source.
If MetaSuccess Then
ADOConn = Server.CreateObject("ADODB.Connection")
ADOConn.Open DataSource, UserName, Password
End If
If you are still using my sample data in the metabase,
these commands will probably error out, unless of course
you've created a DSN called myDSN and
set your system-admin password to "ItsASecret".
But why go to all that trouble? Put your own DSN data
into the metabase and run the code again.
If you have trouble getting the code to access the metabase,
check the spelling of your container names. If you don't see
any mistakes there and you are still having trouble, or if
you are getting messages that say "Permission Denied",
then recheck the steps we discussed in the security section.
You can use MetaACL.vbs to display the access rights for
specific users or all the ACLs of a key. View the source code
for details on how to use MetaACL.vbs.
Where to Go From Here
You've learned how to configure the metabase, set up security,
add keys and data to it, and finally read that data into a web
application. These abilities serve as a foundation that will
allow you to accomplish a great deal using the metabase. Now
that you've completed this project, there are a lot of
things you can do to take this code even further and make it
truly useful. With a little extra work, you can craft this
lesson into a customized code library or component that will
help you manage your usernames and passwords that might
otherwise have been vulnerable. You could even build this
up to the point where you have a tool that could be sold
commercially. At the very least, you can use this code to
retool the database driven ASP applications that you have
now or might create someday.
Why not use code similar to MetaRead.asp in your global.asa
file instead? You could put your metabase read function into
an include file, and call it from global.asa using a single
parameter to specify the key to read from. Of course, for
performance reasons you shouldn't actually create your ADO
connections at the application level, however, you can put
these commands in an include file and have them read the
credentials from application level objects created in
global.asa.
Let's not forget that we could do a lot of work to automate
the process of creating and populating the keys in the metabase.
This goes for securing them as well. Why not build a web
application to manage all of this? Just remember that you
need to force the user to log in as someone for whom you've
assigned write permissions to the DataAccessStorage
key. Because you'd be creating new keys, you would want to
be sure the user has administrator level access before allowing
them to do so.
There really is a lot of uncharted territory here. Custom
settings for inheritance and security can be configured on
your metabase data to make administration easier on you.
And, everything we've done today used only one of the many
metabase data types. If you think it over for a while,
you might be able to come up with a specialized use for this
storage system that I haven't considered. As you play with
the metabase some more, you'll find that it has other
advantages as well, like replication for example.
So what are you waiting for? Get out there and make good
use out of what you've learned.
Code Download
You can download a ZIP file of the code mentioned in this
article from here.