|
This topic appeared first in Ben Forta's column, <BF>
on <CF>, in the ColdFusion
Developer's Journal. It is reprinted here with the permission
of the ColdFusion Developer's Journal.
Databases and database access are fundamental elements of just
about every ColdFusion application ever created. Database access
makes applications real and live and dynamic and valuable, but
it's also a major source of performance problems and a primary
potential security target. In this article I discuss an oft overlooked
tag, <CFQUERYPARAM>,
designed to help address both potential problems.
Understanding <CFQUERYPARAM>
Database access (queries, insertions, updates, etc.) occurs by
issuing SQL statements - plain text strings containing combinations
of special statements, keywords, clauses, operators, and more.
These strings are passed to the DBMS for processing (usually via
a database driver) and are sometimes referred to as being ad
hoc.
So far so good. But this is where it gets a little complicated.
There are an infinite number of possible SQL statements (when
you take into account the values passed to them), so once a DBMS
receives a SQL statement, the DBMS must first analyze it to determine
if it's valid, and then work out the best way to process it. Often,
the process of parsing, validating, and analyzing the query (the
term query refers to all SQL queries, even if they're not
SELECT query operations) takes longer than processing the query
itself.
Once the query is validated and analyzed, the DBMS tries to cache
it so that the process won't need to be repeated unnecessarily.
Simple queries without dynamic elements, like the following one,
can be cached safely and easily:
SELECT *
FROM Customers
However, queries containing other clauses, like the one that
follows, are a little harder to cache. After all, with an infinite
number of variations it wouldn't be practical for the DBMS to
cache them all:
SELECT *
FROM Customers
WHERE CustID=100
To cache this type of query the DBMS needs to know which part
of the statement is dynamic and which is not, and that's where
<CFQUERYPARAM> comes
into play.
<CFQUERYPARAM> is a tag used within SQL statements (inside
your <CFQUERY>). It replaces
parameters (or any passed values) with placeholders that indicate
the dynamic portions of a statement. For example:
<CFQUERY ...>
SELECT *
FROM Customers
WHERE CustID=<CFQUERYPARAM
CFSQLTYPE="CF_SQL_INTEGER"
VALUE="100">
</CFQUERY>
The <CFQUERYPARAM> tag
replaces the passed value (100), and does two things: it defines
the type of data that will be passed here and then passes the
value to be used.
The actual SQL generated by this query will differ from DBMS
to DBMS, but that's somewhat irrelevant. The important thing is
that the DBMS knows that the query containing this block of text
won't change (and can therefore be cached):
SELECT *
FROM Customers
WHERE CustID=
In DBMSese these variable portions of SQL statements, the ones
bookmarked with <CFQUERYPARAM>,
are known as bind parameters.
Specifying SQL Types
The data type passed to CFSQLTYPE tells the DBMS to expect data
of a particular type. The supported types are:
|
|
CF_SQL_BIGINT
|
|
|
CF_SQL_BIT
|
|
|
CF_SQL_CHAR
|
|
|
CF_SQL_DATE
|
|
|
CF_SQL_DECIMAL
|
|
|
CF_SQL_DOUBLE
|
|
|
CF_SQL_FLOAT
|
|
|
CF_SQL_IDSTAMP
|
|
|
CF_SQL_INTEGER
|
|
|
CF_SQL_LONGVARCHAR
|
|
|
CF_SQL_MONEY
|
|
|
CF_SQL_MONEY4
|
|
|
CF_SQL_NUMERIC |
|
|
CF_SQL_REAL
|
|
|
CF_SQL_REFCURSOR
|
|
|
CF_SQL_SMALLINT
|
|
|
CF_SQL_TIME
|
|
|
CF_SQL_TIMESTAMP
|
|
|
CF_SQL_TINYINT
|
|
|
CF_SQL_VARCHAR |
CFSQLTYPE is an optional at-tribute,
but as a rule you should always specify an explicit type. The
default value is CF_SQL_CHAR
(a string) and is frequently not what you need.
Increased Performance
As already implied, the first benefit of using <CFQUERYPARAM>
is increased performance. If the DBMS doesn't have to parse, analyze,
and validate as much text, it'll be able to respond to requests
quicker and more efficiently.
It's common knowledge that stored procedures execute faster than
ad hoc SQL statements. Part of the reason for this is that DBMSs
cache the compiled stored procedures so they won't need to be
processed repeatedly and unnecessarily. By using <CFQUERYPARAM>
you get some of the benefits of stored procedures without having
to write one.
If you're interested in determining what the actual performance
benefit is, turn on ColdFusion debugging output and try the queries
both ways. As the DBMS load increases, so should the benefit of
using bind parameters.
Protecting from Malicious URL Tampering
Note: As a rule I'm staunchly opposed to documenting security
issues and how they're used, but as this particular vulnerability
has been documented extensively (there are even Knowledgebase articles about it at http://www.macromedia.com/) I'm making an
exception this time.
Another benefit to using <CF-QUERYPARAM> is security against
malicious URL tampering. Consider the following example, an adaptation
of the one used previously:
SELECT *
FROM Customers
WHERE CustID=#URL.custid#
Here a WHERE clause is populated dynamically using a URL parameter.
This type of code is common and popular, and is used any time
data drilldown of any kind is used. If the URL was:
http://domain/path/file.cfm?custid=100
the resulting SQL statement would be:
SELECT *
FROM Customers
WHERE CustID=100
But what if someone tampered with that URL so that it read:
http://domain/path/file.cfm?custid=100;DELETE+Customers
The resulting SQL would be:
SELECT *
FROM Customers
WHERE CustID=100;
DELETE Customers
Depending on the DBMS being used, the <CFQUERY>
could end up executing two statements - the SELECT
and then DELETE Customers (which
would promptly delete all data from the Customers table).
Scared? You should be. As I mentioned earlier SQL statements
are not just used for queries. They're also used to create and
drop tables, create user logins, change DBMS passwords, set security
levels, manage scheduled events, even create and drop entire databases.
All features supported by your DBMS may be accessible this way.
Before I go further I must point out that this is not a ColdFusion
vulnerability; it's not even a bug or a hole. This is truly a
feature - many DBMSs do indeed allow queries to contain more than
a single operation; this is legal and by design.
Of course, you should always be checking parameters before passing
them to your DBMS. Passing URL parameters through unchecked is
programmatic suicide. As such, you should already be using code
like this:
<CFIF IsDefined("URL.CustID")
AND NOT IsNumeric(URL.CustID)>
... throw an error or something ...
</CFIF>
But <CFQUERYPARAM> provides
one extra line of defense. If the previous tampered URL was passed
to the following query, the value would be rejected and an error
thrown. The CFSQLTYPE value
also doubles as a datatype validation check, and values that don't
match the type are rejected. Using <CFQUERYPARAM>
you can secure yourself against one of the oldest and best-known
hacks in the book.
Note: This type of hack is one of the reasons you should never
configure ColdFusion to use administrative accounts for database
access (the SA account, for example). As a rule, the login specified
in the data source definition should have just the access it needs
and nothing more. If administrative access is not available to
a login, then some of the more dangerous commands wouldn't be
executable.
Summary
<CFQUERYPARAM> is not
a new tag; it's been in ColdFusion since version 4.5. However,
many developers have yet to use <CFQUERYPARAM>,
primarily because they don't understand it. Most good DBMSs (including
SQL Server and Oracle) support the use of bind parameters, but
some don't. Be sure to read the docs on this tag, and then start
plugging it into any new code and even existing apps. With increased
performance and improved security you'd be remiss not to.
|