Sorcerer's IsleCode QueryParam Scanner / files

  1<!--- qpscanner v0.7.3.2 | (c) Peter Boughton | License: GPLv3 | Website: sorcerersisle.com/projects:qpscanner.html --->
  2<cfcomponent output="false">
  3
  4	<cffunction name="Struct" returntype="Struct" access="private"><cfreturn Arguments/></cffunction>
  5
  6	<cffunction name="init" returntype="any" output="false" access="public">
  7		<cfargument name="jre"                   type="jre-utils"/>
  8		<cfargument name="StartingDir"           type="String"                  hint="Directory to begin scanning the contents of."/>
  9		<cfargument name="OutputFormat"          type="String"  default="html"  hint="Format of scan results: [html,wddx]"/>
 10		<cfargument name="RequestTimeout"        type="Numeric" default="-1"    hint="Override Request Timeout, -1 to ignore"/>
 11		<cfargument name="recurse"               type="Boolean" default="false" hint="Also scan sub-directories?"/>
 12		<cfargument name="Exclusions"            type="String"  default=""      hint="Exclude files & directories matching this regex."/>
 13		<cfargument name="scanOrderBy"           type="Boolean" default="true"  hint="Include ORDER BY statements in scan results?"/>
 14		<cfargument name="scanQoQ"               type="Boolean" default="true"  hint="Include Query of Queries in scan results?"/>
 15		<cfargument name="scanBuiltInFunc"       type="Boolean" default="true"  hint="Include Built-in Functions in scan results?"/>
 16		<cfargument name="showScopeInfo"         type="Boolean" default="true"  hint="Show scope information in scan results?"/>
 17		<cfargument name="highlightClientScopes" type="Boolean" default="true"  hint="Highlight scopes with greater risk?"/>
 18		<cfargument name="ClientScopes"          type="String"  default="form,url,client,cookie" hint="Scopes considered client scopes."/>
 19		<cfargument name="NumericFunctions"      type="String"  default="val,year,month,day,hour,minute,second,asc,dayofweek,dayofyear,daysinyear,quarter,week,fix,int,round,ceiling,gettickcount,len,min,max,pi,arraylen,listlen,structcount,listvaluecount,listvaluecountnocase,rand,randrange"/>
 20		<cfargument name="BuiltInFunctions"      type="String"  default="now,#Arguments.NumericFunctions#"/>
 21
 22		<cfset var Arg = -1/>
 23		<cfset var RegexList = ""/>
 24		<cfset var Rex = ""/>
 25		<cfset var cf = 'cf'/>
 26
 27		<cfloop item="Arg" collection="#Arguments#">
 28			<cfset This[Arg] = Arguments[Arg]/>
 29		</cfloop>
 30
 31		<cfset Variables.jre = Arguments.jre/>
 32		<cfset StructDelete(This,'jre')/>
 33
 34		<cfset This.Totals = Struct
 35			( AlertCount: 0
 36			, QueryCount: 0
 37			, FileCount : 0
 38			, DirCount  : 0
 39			, Time      : 0
 40			)/>
 41
 42		<cfset This.Timeout = false/>
 43
 44		<cfset Variables.ResultFields = "FileId,FileName,QueryAlertCount,QueryTotalCount,QueryId,QueryName,QueryStartLine,QueryEndLine,ScopeList,ContainsClientScope,QueryCode"/>
 45		<cfset Variables.AlertData = QueryNew(Variables.ResultFields)/>
 46
 47		<cfsavecontent variable="RegexList"><cfoutput>
 48			findQueries      |(?si)(<#cf#query[^p]).*?(?=</#cf#query>)
 49			findQueryTag     |(?si)(<#cf#query(?!p)[^>]{0,300}>)
 50			isQueryOfQuery   |(?si)dbtype\s*=\s*["']query["']
 51			killParams       |(?si)<#cf#queryparam[^>]+>
 52			killCfTag        |(?si)<#cf#[a-z]{2,}[^>]*> <!--- Deliberately excludes Custom Tags and CFX --->
 53			killOrderBy      |(?si)\bORDER BY\b.*?$
 54			killBuiltIn      |(?si)##(#ListChangeDelims(This.BuiltInFunctions,'|')#)\([^)]*\)##
 55			findScopes       |(?si)(?<=##([a-z]{1,20}\()?)[^\(##<]+?(?=\.[^##<]+?##)
 56			findName         |(?si)(?<=(<#cf#query[^>]{0,300}\bname=")).*?(?="[^>]{0,300}>)
 57			findClientScopes |(?i)\b(#ListChangeDelims(This.ClientScopes,'|')#)\b
 58			isCfmlFile       |(?i)\.cf(c|ml?)$
 59		</cfoutput></cfsavecontent>
 60
 61		<cfloop index="Rex" list="#RegexList#" delimiters="#Chr(10)#">
 62			<cfif Len(Trim(Rex))>
 63				<cfset Variables.Regexes[ Trim(ListFirst(Rex,'|')) ] = Trim(ListRest(Rex,'|'))/>
 64			</cfif>
 65		</cfloop>
 66
 67		<cfreturn This/>
 68	</cffunction>
 69
 70
 71
 72	<cffunction name="go" returntype="any" output="false" access="public">
 73		<cfset var StartTime = getTickCount()/>
 74
 75		<cfif This.RequestTimeout GT 0>
 76			<cfsetting requesttimeout="#This.RequestTimeout#"/>
 77		</cfif>
 78
 79		<cftry>
 80			<cfset scan(This.StartingDir)/>
 81
 82			<!--- TODO: MINOR: CHECK: Is this the best way to handle this? --->
 83			<!--- If timeout occurs, ignore error and proceed. --->
 84			<cfcatch>
 85				<cfif find('timeout',cfcatch.message)>
 86					<cfset This.Timeout = True/>
 87				<cfelse>
 88					<cfrethrow/>
 89				</cfif>
 90			</cfcatch>
 91		</cftry>
 92
 93		<cfset This.Totals.Time = getTickCount() - StartTime/>
 94		<cfreturn Struct
 95			( Data : Variables.AlertData
 96			, Info : Struct
 97				( Totals  : This.Totals
 98				, Timeout : This.Timeout
 99				)
100			)/>
101	</cffunction>
102
103
104
105	<cffunction name="scan" returntype="void" output="false" access="public">
106		<cfargument name="DirName"           type="string"/>
107		<cfset var qryDir     = -1/>
108		<cfset var qryCurData = -1/>
109		<cfset var CurrentTarget = -1/>
110		<cfset var process = true/>
111		<cfset var jre = Variables.jre/>
112
113		<cfif DirectoryExists(Arguments.DirName)>
114
115			<cfdirectory
116				name="qryDir"
117				directory="#Arguments.DirName#"
118				sort="type ASC,name ASC"
119			/>
120
121			<cfloop query="qryDir">
122
123				<cfset CurrentTarget = Arguments.DirName & '/' & Name />
124
125
126				<cfset process = true/>
127				<cfloop index="CurrentExclusion" list="#This.Exclusions#" delimiters=";">
128					<cfif jre.matches( CurrentTarget , CurrentExclusion )>
129						<cfset process = false/>
130					</cfif>
131				</cfloop>
132
133				<cfif process>
134
135					<cfif (Type EQ "dir") AND This.recurse >
136						<cfset This.Totals.DirCount = This.Totals.DirCount + 1 />
137
138						<cfset scan( CurrentTarget )/>
139
140					<cfelseif jre.matches( CurrentTarget , Variables.Regexes.isCfmlFile )>
141						<cfset This.Totals.FileCount = This.Totals.FileCount + 1 />
142
143						<cfset qryCurData = hunt( CurrentTarget )/>
144
145						<cfif qryCurData.RecordCount>
146							<cfset Variables.AlertData = QueryAppend( Variables.AlertData , qryCurData )/>
147						</cfif>
148
149					</cfif>
150
151				</cfif>
152			</cfloop>
153
154		<!--- This can only potentially trigger on first iteration, if This.StartingDir is a file. --->
155		<cfelseif FileExists(Arguments.DirName)>
156			<cfset This.Totals.FileCount = This.Totals.FileCount + 1 />
157
158			<cfset qryCurData = hunt( This.StartingDir )/>
159
160			<cfif qryCurData.RecordCount>
161				<cfset Variables.AlertData = QueryAppend( Variables.AlertData , qryCurData )/>
162			</cfif>
163		</cfif>
164
165	</cffunction>
166
167
168
169
170	<cffunction name="hunt" returntype="Query" output="false">
171		<cfargument name="FileName"    type="String"/>
172		<cfset var FileData        = -1/>
173		<cfset var Matches         = -1/>
174		<cfset var i               = -1/>
175		<cfset var info            = -1/>
176		<cfset var rekCode         = -1/>
177		<cfset var QueryCode       = -1/>
178		<cfset var CurRow          = -1/>
179		<cfset var CurFileId       = -1/>
180		<cfset var StartLine       = -1/>
181		<cfset var LineCount       = -1/>
182		<cfset var BeforeQueryCode = -1/>
183		<cfset var isRisk          = -1/>
184		<cfset var UniqueToken = Chr(65536)/>
185		<cfset var qryResult   = QueryNew(Variables.ResultFields)/>
186		<cfset var REX = Variables.Regexes/>
187		<cfset var jre = Variables.jre/>
188
189
190		<cffile action="read" file="#Arguments.FileName#" variable="FileData"/>
191
192		<cfset Matches = jre.get( FileData , REX.findQueries )/>
193		<cfset This.Totals.QueryCount = This.Totals.QueryCount + ArrayLen(Matches) />
194
195		<cfloop index="i" from="1" to="#ArrayLen(Matches)#">
196
197			<cfset QueryCode = jre.replace( Matches[i] , REX.findQueryTag , '' , 'ALL' )/>
198			<cfset rekCode = duplicate(QueryCode) />
199			<cfset rekCode = jre.replace( rekCode    , REX.killParams , '' , 'ALL' )/>
200			<cfset rekCode = jre.replace( rekCode    , REX.killCfTag  , '' , 'ALL' )/>
201
202			<cfif NOT This.scanOrderBy>
203				<cfset rekCode = jre.replace( rekCode , REX.killOrderBy , '' , 'ALL' )/>
204			</cfif>
205			<cfif NOT This.scanBuiltInFunc>
206				<cfset rekCode = jre.replace( rekCode , REX.killBuiltIn , '' , 'ALL' )/>
207			</cfif>
208
209			<cfset isRisk = find( '##' , rekCode )/>
210
211
212			<cfif (NOT This.scanQoQ) AND jre.matches( Matches[i] , REX.isQueryOfQuery )>
213				<cfset isRisk = false/>
214			</cfif>
215
216
217			<cfif isRisk>
218				<cfset CurRow = QueryAddRow(qryResult)/>
219
220				<cfset qryResult.QueryCode[CurRow] = jre.replace( QueryCode , Chr(13) , Chr(10) , 'all' ) />
221				<cfset qryResult.QueryCode[CurRow] = jre.replace( qryResult.QueryCode[CurRow] , Chr(10)&Chr(10) , Chr(10) , 'all' ) />
222				<cfif This.showScopeInfo >
223					<cfset qryResult.ScopeList[CurRow] = ArrayToList( ArrayUnique( jre.get( rekCode , REX.findScopes ) ) ) />
224
225					<cfset qryResult.ContainsClientScope[CurRow] = false/>
226					<cfif This.highlightClientScopes>
227						<cfloop index="CurrentScope" list="#This.ClientScopes#">
228							<cfif ListFind( qryResult.ScopeList[CurRow] , CurrentScope )>
229								<cfset qryResult.ContainsClientScope[CurRow] = true/>
230								<cfbreak/>
231							</cfif>
232						</cfloop>
233					</cfif>
234				</cfif>
235
236				<!--- CF8 doesn't support get()[1] so need to use two lines: --->
237				<cfset QueryTagCode = jre.get( Matches[i] , REX.findQueryTag )/>
238				<cfset QueryTagCode = QueryTagCode[1] />
239
240				<cfset BeforeQueryCode = ListFirst ( replace ( ' '&FileData&' ' , Matches[i] , UniqueToken ) , UniqueToken )/>
241
242
243				<cfset StartLine = 1+ArrayLen( jre.get( BeforeQueryCode , chr(10) ) )/>
244				<cfset LineCount = ArrayLen( jre.get( Matches[i] , chr(10) ) )/>
245
246
247				<cfset qryResult.QueryStartLine[CurRow] = StartLine/>
248				<cfset qryResult.QueryEndLine[CurRow]   = StartLine + LineCount />
249				<cfset qryResult.QueryName[CurRow]      = ArrayToList( jre.get( ListLast(QueryTagCode,chr(10)) , REX.findName ) )/>
250				<cfset qryResult.QueryId[CurRow]        = createUuid() />
251				<cfif NOT Len( qryResult.QueryName[CurRow] )>
252					<cfset qryResult.QueryName[CurRow] = "[unknown]"/>
253				</cfif>
254
255			</cfif>
256
257		</cfloop>
258
259		<cfset CurFileId = createUUID()/>
260		<cfloop query="qryResult">
261			<cfset qryResult.FileId[qryResult.CurrentRow]          = CurFileId />
262			<cfset qryResult.FileName[qryResult.CurrentRow]        = Arguments.FileName />
263			<cfset qryResult.QueryTotalCount[qryResult.CurrentRow] = ArrayLen(Matches) />
264			<cfset qryResult.QueryAlertCount[qryResult.CurrentRow] = qryResult.RecordCount />
265		</cfloop>
266		<cfset This.Totals.AlertCount = This.Totals.AlertCount + qryResult.RecordCount />
267
268		<cfreturn qryResult/>
269	</cffunction>
270
271
272
273
274
275
276
277
278	<cffunction name="ArrayUnique" returntype="Array" output="false" access="private">
279		<cfargument name="ArrayVar" type="Array"/>
280		<cfset var UniqueToken = Chr(65536)/>
281		<cfset var Result = duplicate(Arguments.ArrayVar)/>
282		<cfset ArraySort(Result,'text')/>
283		<cfset Result = ArrayToList( Result , UniqueToken )/>
284		<!--- TODO: MINOR: FIX: Using \b works for the ScopeList, but is not good enough for general use - why not using UniqueToken? --->
285		<cfset Result = REreplace( Result & UniqueToken , '(\b(.*?)\b)\1+' , '\1' , 'all' )/>
286		<cfset Result = ListToArray( Result , UniqueToken )/>
287		<!--- TODO: MINOR: Ideally, the original array order should be restored. --->
288		<cfreturn Result/>
289	</cffunction>
290
291
292
293	<cffunction name="QueryAppend" returntype="Query" output="false" access="private">
294		<cfargument name="QueryOne" type="Query"/>
295		<cfargument name="QueryTwo" type="Query"/>
296		<cfset var Result = -1/>
297		<!--- Bug fix for CF8 --->
298		<cfif NOT Arguments.QueryOne.RecordCount><cfreturn Arguments.QueryTwo /></cfif>
299		<cfif NOT Arguments.QueryTwo.RecordCount><cfreturn Arguments.QueryOne /></cfif>
300		<!--- / --->
301		<cfquery name="Result" dbtype="Query">
302			SELECT * FROM Arguments.QueryOne
303			UNION SELECT * FROM Arguments.QueryTwo
304		</cfquery>
305		<cfreturn Result/>
306	</cffunction>
307
308
309
310
311</cfcomponent>