3
votes

Attempting to use a variable containing a comma separated set of column updates. The variable ends up containing valid SQL (or more precisely valid Coldfusion syntax), but the query throws an error: [Macromedia][SQLServer JDBC Driver][SQLServer]Incorrect syntax near '<'.

I start off with an initial column to update:

<cfset sqlString = myColumn & ' = <cfqueryparam value="#myValue#" cfsqltype="#myDataType#" null="#NOT len(trim(myValue))#">'>

Then loop through some form elements doing this:

<cfset sqlString = sqlString & ', ' & myColumn & ' = <cfqueryparam value="#myValue#" cfsqltype="#myDataType#" null="#NOT len(trim(myValue))#">'>

I end up of course with an unbroken string in sqlString, but for readability, it contains something like this:

id = <cfqueryparam value="123" cfsqltype="cf_sql_integer">,
csp = <cfqueryparam value="5" cfsqltype="cf_sql_integer">,
Q185 = <cfqueryparam value="" cfsqltype="cf_sql_integer" null="YES">,
Q3 = <cfqueryparam value="" cfsqltype="cf_sql_integer" null="YES">,
Q177 = <cfqueryparam value="" cfsqltype="cf_sql_date" null="YES">

So I attempt to use that in a an update query like this:

<cfquery name="update_answers" datasource="#application.datasource#">
   UPDATE answers
   SET    #PreserveSingleQuotes(sqlString)#                                                     
   WHERE  rec_id = #id#
</cfquery>

I've tried this with and without the PreserveSingleQuotes function to no avail.

If I output the contents of sqlString and paste it directly into the query like so, it works fine:

<cfquery name="update_answers" datasource="#application.datasource#">
   UPDATE answers
          id = <cfqueryparam value="123" cfsqltype="cf_sql_integer">,
          csp = <cfqueryparam value="5" cfsqltype="cf_sql_integer">,
          Q185 = <cfqueryparam value="" cfsqltype="cf_sql_integer" null="YES">,
          Q3 = <cfqueryparam value="" cfsqltype="cf_sql_integer" null="YES">,
          Q177 = <cfqueryparam value="" cfsqltype="cf_sql_date" null="YES">                                                    
   WHERE  rec_id = #id#
</cfquery>

Again, I'm showing line breaks here for readability, but it doesn't matter if I paste the sqlString contents into the query with or without line breaks; it WILL work.

Any ideas?

2
I completely agree with Ageax's suggestions. SQL Injection is a very big concern with pretty much any kind of dynamic SQL, especially when it's coming from an untrusted source like a form. If you have to build this string, I would highly recommend that you build it up in cfscript and put it through some hardcore validation as soon as it comes in from the form. - Shawn
What version of ColdFusion and SQL? - Shawn
And how exactly are you passing myColumn, myValue and myDatatype? - Shawn
And where does rec_id's #id# come from? - Shawn
So to add a little context, I'm dealing with an application I inherited that has a multitude of issues that I can't even begin to address here. It is beyond ugly. The code I've provided is really only intended to illustrate the core question I had regarding the failure of the SQL update when the 'sqlString' variable is used, while it doesn't fail if the content of that variable is substituted in. I think Ageax answered that. - mpaul

2 Answers

6
votes

tldr;

CFQueryparam cannot be nested within a string. It must be used directly within <cfquery> tags. To build sql statements, with dynamic parameters, take a look at the cfscript equivalent of cfqueryparam.

Security Issues

While you probably only tried PreserveSingleQuotes() out of desperation, never use that function unless you understand the repercussions, because it basically creates a great big sql injection hole in your application.

<cfset sqlString = sqlString & ', ' & myColumn

Also, be very careful with that sort of dynamic sql statement. Even if you protect all of the parameters with cfqueryparam, the query is still vulnerable to sql injection because myColumn is a user supplied value. Unfortunately cfqueryparam can't protect object names (table names, column names, etc..), only literals (string, date, etc..). So if you absolutely must use dynamic column names in raw sql, be sure to validate them against a white list and reject the request if invalid columns are detected.

0
votes

You could use the following approach. I wrote this routine a few years ago, when I needed to build up an array of cfquerparams, dynamically.

I have added the 'ReadQuery' function, aswell, just in case, you need to use this, in future.

It has been thoroughly tested on Lucee 4.5 (Windows 2008R2) & ACF11 (Windows 10).

If you require further instruction on the calling sysntax, please let me know, but I have added an example at the bottom, to get you started.

The functions are actually part of a CFC service, but you can use them in a standalone manner:


<!--- FUNCTION UDF: build SQL params --->

<!--- 

Notes:
Use for building sql with simple read queries: 

Supported:

- INNER JOINS
- value paramaterization
- IN operator with or without paramaterization
- ignore WHERE clause item, if a certain value or a value in a list of values, is matched: {...'ignore'='value'}
- ignore value list custom delimeter: default {...'ignoredelimiter'=','}
- use {...'sqltype'='number|string'} for non value paramaterization 
- positional paramaters 'table column ?'

Unsupported:

- subqueries
- named parameters, 'table column = :table column', due to a bug with the way Railo parses table prefixed columns sql

--->

<cffunction name="BuildSQLParams" access="public" returntype="string" hint="function description: build SQL params">
  <cfargument name="query" required="false" default="#StructNew()#" type="struct" hint="argument description: query">
  <cfargument name="params" required="false" default="#ArrayNew(1)#" type="array" hint="argument description: params">
  <cfset var result =  "">
  <cfset var local = StructNew()>
  <cfset local.keylist = "column,operator,sqltype,value">
  <cfset local.sqlarray = ArrayNew(1)>
  <cfif IsStruct(arguments.query) AND ArrayLen(arguments.params)>
    <cfloop from="1" to="#ArrayLen(arguments.params)#" index="local.i">
      <cfset local.param = arguments.params[local.i]>
      <cfif IsStruct(local.param) AND NOT StructIsEmpty(local.param)>
        <cfset local.isValidParam = false>
        <cfset local.counter = 0>
        <cfloop collection="#local.param#" item="local.key">
          <cfif ListFindNoCase(local.keylist,local.key)>
            <cfset local.counter = local.counter + 1>
          </cfif>
        </cfloop>
        <cfif local.counter EQ ListLen(local.keylist)>
        <cfset local.isValidParam = true>
        </cfif>
        <cfif local.isValidParam>
        <cfset local.ignore = false>
        <cfset local.ignoredelimiter = ",">
          <cfif StructKeyExists(param,'ignoredelimiter')>
            <cfset local.ignoredelimiter = param['ignoredelimiter']>
          </cfif>
          <cfif StructKeyExists(param,'ignore')>
            <cfif ListLen(param['ignore'],local.ignoredelimiter)>
              <cfif ListFindNoCase(param['ignore'],param['value'],local.ignoredelimiter)>
                <cfset local.ignore = true>
              </cfif>
            <cfelse>
              <cfif param['ignore'] EQ param['value']>
                <cfset local.ignore = true>
              </cfif>
            </cfif>
          </cfif>
          <cfif NOT local.ignore>
          <cfif NOT ListFindNoCase("number,string",param['sqltype'])>
              <cfif param['operator'] EQ "IN">
                <cfset arguments.query.addParam(value=param['value'],cfsqltype="cf_sql_#param['sqltype']#",list="yes")>
              <cfelse>
                <cfset arguments.query.addParam(value=param['value'],cfsqltype="cf_sql_#param['sqltype']#")>
              </cfif>
            </cfif>
          </cfif>
          <cfif NOT local.ignore>
            <cfsavecontent variable="local.sql">
              <cfoutput>
                #param['column']# 
                #param['operator']# 
                <cfif ListFindNoCase("number,string",param['sqltype'])>
                  <cfif param['sqltype'] EQ "number">
                    #param['value']# 
                  <cfelse>
                    '#param['value']#' 
                  </cfif>
                <cfelse>
                <cfif param['operator'] EQ "IN">
                    (?) 
                  <cfelse>
                    ?
                  </cfif>
                </cfif>
                <cfif StructKeyExists(param,'andOr')>
                  #param['andOr']# 
                </cfif>
              </cfoutput>
            </cfsavecontent>
            <cfset result = result & local.sql>
          </cfif>
        </cfif>
      </cfif>
    </cfloop>
  </cfif>     
  <cfif Len(Trim(result))>
  <cfset result = REReplaceNoCase(result,"[\s]+"," ","ALL")>
    <cfset result = REReplaceNoCase(result,"[\s]+(AND|OR|,)[\s]*$","","ALL")>
    <cfset result = Trim(result)>
  </cfif>
<cfreturn result /> 
</cffunction>


<!--- FUNCTION UDF: read query --->

<!--- 

Notes:
Use for executing sql with simple read queries: 

Supported:

- query attributes: datasource

Unsupported:

- apart from 'datasource', no other query attributes are supported

--->

<cffunction name="ReadQuery" returntype="struct" output="false" access="public" hint="function description: read query">
  <!--- arguments --->
  <cfargument name="dsn" required="yes" hint="argument description: dsn">
  <cfargument name="columns" type="string" required="no" default="" hint="argument description: columns">
  <cfargument name="tables" type="string" required="no" default="" hint="argument description: tables">
  <cfargument name="params" required="false" default="#ArrayNew(1)#" type="array" hint="argument description: params">
  <cfargument name="groupby" type="string" required="no" default="" hint="argument description: group by">
  <cfargument name="orderby" type="string" required="no" default="" hint="argument description: order by">
  <cfargument name="sortorder" type="string" required="no" default="ASC" hint="argument description: sort order">
  <!--- local variables --->
  <cfset var result = StructNew()>
  <cfset var local = StructNew()>
  <!--- logic --->
  <cfset StructInsert(result,"query",QueryNew(''))>
  <cfset StructInsert(result,"metaInfo",StructNew())>
  <cfif Len(Trim(arguments.tables))>
  <cfset local.wheresql = "">
    <cfset local.groupby = "">
    <cfset local.orderby = "">
    <cfif Len(Trim(arguments.groupby))>
      <cfset local.groupby = " GROUP BY " & arguments.groupby>
    </cfif>
    <cfif Len(Trim(arguments.orderby))>
      <cfset local.orderby = " ORDER BY " & arguments.orderby & " " & arguments.sortorder>
    </cfif>
  <cfset local.query = new Query()> 
    <cfset local.query.setAttributes(datasource=arguments.dsn)> 
    <cfset local.query.setAttributes(name="result")>
    <cfif ArrayLen(arguments.params)>
      <cfset local.wheresql = BuildSQLParams(local.query,arguments.params)>
    </cfif>
    <cfif Len(Trim(local.wheresql))>
      <cfset local.wheresql = " WHERE " & local.wheresql>
    </cfif>
    <cfset local.execute = local.query.execute(sql="SELECT #arguments.columns# FROM #arguments.tables##local.wheresql##local.groupby##local.orderby#")>
    <cfset result.query = local.execute.getResult()> 
    <cfset result.metaInfo = local.execute.getPrefix()>
  </cfif>
<cfreturn result>
</cffunction>


<!--- FUNCTION UDF: update query --->

<!--- 

Notes:
Use for executing sql with simple update queries: 

Supported:

- query attributes: datasource

Unsupported:

- apart from 'datasource', no other query attributes are supported

--->

<cffunction name="UpdateQuery" returntype="struct" output="false" access="public" hint="function description: read query">
  <!--- arguments --->
  <cfargument name="dsn" required="yes" hint="argument description: dsn" />
  <cfargument name="tables" type="string" required="no" default="" hint="argument description: tables">
  <cfargument name="setparams" required="false" default="#ArrayNew(1)#" type="array" hint="argument description: set params">
  <cfargument name="whereparams" required="false" default="#ArrayNew(1)#" type="array" hint="argument description: where params">
  <!--- local variables --->
  <cfset var result = StructNew()>
  <cfset var local = StructNew()>
  <!--- logic --->
  <cfset StructInsert(result,"query",QueryNew(''))>
  <cfset StructInsert(result,"metaInfo",StructNew())>
  <cfif Len(Trim(arguments.tables))>
  <cfset local.setsql = "">
  <cfset local.wheresql = "">
  <cfset local.query = new Query()> 
    <cfset local.query.setAttributes(datasource=arguments.dsn)> 
    <cfset local.query.setAttributes(name="result")>
    <cfif ArrayLen(arguments.setparams)>
      <cfset local.setsql = BuildSQLParams(local.query,arguments.setparams)>
    </cfif>
    <cfif Len(Trim(local.setsql))>
      <cfset local.setsql = " SET " & local.setsql>
    </cfif>
    <cfif ArrayLen(arguments.whereparams)>
      <cfset local.wheresql = BuildSQLParams(local.query,arguments.whereparams)>
    </cfif>
    <cfif Len(Trim(local.wheresql))>
      <cfset local.wheresql = " WHERE " & local.wheresql>
    </cfif>
    <cfset local.execute = local.query.execute(sql="UPDATE #arguments.tables##local.setsql##local.wheresql#")>
    <cfset result.query = local.execute.getResult()> 
    <cfset result.metaInfo = local.execute.getPrefix()>
  </cfif>
<cfreturn result>
</cffunction>

<cfset sqlarray = ArrayNew(1)>
<cfset wherearray = ArrayNew(1)>

<cfset ArrayAppend(sqlarray,{'column'='id','operator'='=','sqltype'='integer','value'='123','andOr'=','})>

<cfset ArrayAppend(sqlarray,{'column'='csp','operator'='=','sqltype'='integer','value'='5','andOr'=','})>

...

<cfset ArrayAppend(wherearray,{'column'='rec_id','operator'='=','sqltype'='integer','value'=id,'andOr'=''}>

<cfset UpdateQuery(dsn=application.datasource,tables="answers",setparams=sqlarray,whereparams=wherearray)>