Sorcerer's IsleCode QueryParam Scanner / files

  1<!--- qpscanner v0.8 | (c) Peter Boughton | License: GPLv3 | Website: https://www.sorcerersisle.com/software/qpscanner --->
  2<cfcomponent output=false>
  3
  4	<cffunction name="init" returntype="any" output=false access="public">
  5		<cfargument name="StartingDir"           type="String"  required_       hint="Directory to begin scanning the contents of." />
  6		<cfargument name="OutputFormat"          type="String"  default="html"  hint="Format of scan results: [html,wddx]" />
  7		<cfargument name="RequestTimeout"        type="Numeric" default="-1"    hint="Override Request Timeout, -1 to ignore" />
  8		<cfargument name="recurse"               type="Boolean" default=false   hint="Also scan sub-directories?" />
  9		<cfargument name="Exclusions"            type="String"  default=""      hint="Exclude files & directories matching this regex." />
 10		<cfargument name="scanOrderBy"           type="Boolean" default=true    hint="Include ORDER BY statements in scan results?" />
 11		<cfargument name="scanQoQ"               type="Boolean" default=true    hint="Include Query of Queries in scan results?" />
 12		<cfargument name="scanBuiltInFunc"       type="Boolean" default=true    hint="Include Built-in Functions in scan results?" />
 13		<cfargument name="showScopeInfo"         type="Boolean" default=true    hint="Show scope information in scan results?" />
 14		<cfargument name="highlightClientScopes" type="Boolean" default=true    hint="Highlight scopes with greater risk?" />
 15		<cfargument name="ClientScopes"          type="String"  default="form,url,client,cookie" hint="Scopes considered client scopes." />
 16		<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" />
 17		<cfargument name="BuiltInFunctions"      type="String"  default="now,#Arguments.NumericFunctions#" />
 18		<cfargument name="ReturnSqlSegments"     type="Boolean" default=false   hint="Include separate SELECT/FROM/WHERE/etc in result data?" />
 19
 20		<cfloop item="local.Arg" collection=#Arguments# >
 21			<cfset This[Arg] = Arguments[Arg]/>
 22		</cfloop>
 23
 24		<cfset This.ClientScopes = ListToArray(This.ClientScopes) />
 25
 26		<cfset This.Totals =
 27			{ AlertCount    = 0
 28			, QueryCount    = 0
 29			, FileCount     = 0
 30			, RiskFileCount = 0
 31			, Time          = 0
 32			}/>
 33
 34		<cfset This.Timeout = false />
 35
 36		<cfset Variables.ResultFields = "FileId,FileName,QueryAlertCount,QueryTotalCount,QueryId,QueryName,QueryStartLine,QueryEndLine,ScopeList,ContainsClientScope,QueryCode,FilteredCode" />
 37
 38		<cfset Variables.Regexes =
 39			{ findQueries      = new cfregex( '(?si)(?:<cfquery\b)(?:[^<]++|<(?!/cfquery>))+(?=</cfquery>)' )
 40			, isQueryOfQuery   = new cfregex( '(?si)(?<=\s)dbtype\s*=\s*(["'']?)query\1' )
 41			, killParams       = new cfregex( '(?si)<cfqueryparam[^>]++>' )
 42			, killCfTag        = new cfregex( '(?si)<cf[a-z]{2,}[^>]*+>' ) <!--- Deliberately excludes Custom Tags and CFX --->
 43			, killOrderBy      = new cfregex( '(?si)\bORDER BY\b.*?$' )
 44			, killBuiltIn      = new cfregex( '(?si)##(#ListChangeDelims(This.BuiltInFunctions,'|')#)\([^)]*\)##' )
 45			, killEscapedHash  = new cfregex ('(?:##{2})++')
 46			, findScopes       = new cfregex( '(?si)(?<=##([a-z]{1,20}\()?)[^\(##<]+?(?=\.[^##<]+?##)' )
 47			, findQueryName    = new cfregex( '(?<=\bname\s{0,99}=\s{0,99})(?:"[^"]++"|''[^'']++''|[^"''\s]++)' )
 48			, Newline          = new cfregex( chr(10) )
 49			}/>
 50
 51		<cfif This.ReturnSqlSegments >
 52			<cfset Variables.ResultFields &= ',QuerySegments' />
 53			<cfset var SegKeywords = '(?i:SELECT|FROM|WHERE|GROUP BY|HAVING|ORDER BY)' />
 54
 55			<cfsavecontent variable="Variables.Regexes.Segs"><cfoutput>
 56				(?x)
 57
 58				## Segment names must be preceeded by newline or paren.
 59				## This helps avoid strings/variables causing confusion.
 60				(?<=
 61					(?:^|[()\n])
 62					\s{0,99}
 63				)
 64				#SegKeywords#[\s(]
 65
 66				## This part needs to lazily consume content until it finds
 67				## the next segment, whilst also making sure it's not
 68				## dealing with a [bracketed] column name.
 69				##
 70				## For performance, splitting out whitespace and parens
 71				## allows the negative charset to match possessively
 72				## without breaking the overall laziness.
 73				(?:
 74					[^\[\s()]++
 75				|
 76					[\s()]+
 77				|
 78					\[(?!\s*#SegKeywords#\s*\])
 79				)+?
 80
 81				## A segment must be ended by either end of string or
 82				## another segment.
 83				(?=
 84					$
 85				|
 86					(?<=[)\s])#SegKeywords#[\s(]
 87				)
 88			</cfoutput></cfsavecontent>
 89			<cfset Variables.Regexes.Segs = new cfregex(trim(Variables.Regexes.Segs)) />
 90
 91			<cfset Variables.Regexes.SegNames = new cfregex('(?<=^#SegKeywords#)') />
 92		</cfif>
 93
 94		<cfset Variables.AlertData = QueryNew(Variables.ResultFields)/>
 95
 96		<cfset Variables.Exclusions = [] />
 97		<cfloop index="local.CurrentExclusion" list=#This.Exclusions# delimiters=";" >
 98			<cfset ArrayAppend( Variables.Exclusions , new cfregex(CurrentExclusion) ) />
 99		</cfloop>
100
101		<cfreturn This/>
102	</cffunction>
103
104
105	<cffunction name="go" returntype="Struct" output=false access="public">
106		<cfset var StartTime = getTickCount()/>
107
108		<cfif This.RequestTimeout GT 0>
109			<cfsetting requesttimeout=#This.RequestTimeout# />
110		</cfif>
111
112		<cftry>
113			<cfset scan(This.StartingDir) />
114
115			<!--- TODO: MINOR: CHECK: Is this the best way to handle this? --->
116			<!--- If timeout occurs, ignore error and proceed. --->
117			<cfcatch>
118				<cfif find('timeout',cfcatch.message)>
119					<cfset This.Timeout = true />
120				<cfelse>
121					<cfrethrow/>
122				</cfif>
123			</cfcatch>
124		</cftry>
125
126		<cfset This.Totals.Time = getTickCount() - StartTime/>
127
128		<cfreturn
129			{ Data = Variables.AlertData
130			, Info =
131				{ Totals  = This.Totals
132				, Timeout = This.Timeout
133				}
134			}/>
135	</cffunction>
136
137
138	<cffunction name="scan" returntype="void" output=false access="private">
139		<cfargument name="DirName" type="string" required_ />
140
141		<cfif DirectoryExists(Arguments.DirName)>
142
143			<cfdirectory
144				name      = "local.qryDir"
145				directory = #Arguments.DirName#
146				sort      = "type ASC,name ASC"
147			/>
148
149			<cfloop query="qryDir">
150				<cfset var CurrentTarget = Arguments.DirName & '/' & Name />
151
152				<cfset var process = true />
153				<cfloop index="local.CurrentExclusion" array=#Variables.Exclusions# >
154					<cfif CurrentExclusion.matches( CurrentTarget )>
155						<cfset process = false />
156						<cfbreak />
157					</cfif>
158				</cfloop>
159				<cfif NOT process> <cfcontinue/> </cfif>
160
161				<cfif (Type EQ "dir") AND This.recurse >
162
163					<cfset scan( CurrentTarget )/>
164
165				<cfelseif Type EQ "file">
166
167					<cfset var Ext = LCase(ListLast(CurrentTarget,'.')) >
168
169					<cfif Ext EQ 'cfc' OR Ext EQ 'cfm' OR Ext EQ 'cfml'>
170
171						<cfset var qryCurData = hunt( CurrentTarget )/>
172
173						<cfif qryCurData.RecordCount>
174							<cfset QueryAppend( Variables.AlertData , qryCurData )/>
175						</cfif>
176
177					</cfif>
178
179				</cfif>
180
181			</cfloop>
182
183		<!--- This can only potentially trigger on first iteration, if This.StartingDir is a file. --->
184		<cfelseif FileExists(Arguments.DirName)>
185
186			<cfset var qryCurData = hunt( This.StartingDir )/>
187
188			<cfif qryCurData.RecordCount>
189				<cfset QueryAppend( Variables.AlertData , qryCurData )/>
190			</cfif>
191
192		<cfelse>
193
194			<cfthrow
195				message = "Specified path [#Arguments.DirName#] cannot be accessed or does not exist."
196				type    = "qpscanner.qpscanner.Scan.InvalidPath"
197			/>
198		</cfif>
199
200	</cffunction>
201
202
203	<cffunction name="hunt" returntype="Query" output=false access="private">
204		<cfargument name="FileName" type="String" required_ />
205		<cfset var qryResult = QueryNew(Variables.ResultFields) />
206
207		<cfset local.FileData = FileRead(Arguments.FileName) />
208
209		<cfset var Matches = Variables.Regexes['findQueries'].find( text=FileData , returntype='info' ) />
210
211		<cfloop index="CurMatch" array=#Matches# >
212
213			<cfset var QueryTagCode = ListFirst( CurMatch.Match , '>' ) />
214			<cfset var QueryCode    = ListRest( CurMatch.Match , '>' ) />
215
216			<cfset var rekCode = Variables.Regexes['killParams'].replace( QueryCode , '' )/>
217			<cfset rekCode     = Variables.Regexes['killCfTag'].replace( rekCode , '' )/>
218
219			<cfif NOT This.scanOrderBy>
220				<cfset rekCode = Variables.Regexes['killOrderBy'].replace( rekCode , '' )/>
221			</cfif>
222			<cfif NOT This.scanBuiltInFunc>
223				<cfset rekCode = Variables.Regexes['killBuiltIn'].replace( rekCode , '' )/>
224			</cfif>
225
226			<cfset rekCode =  Variables.Regexes['killEscapedHash'].replace( rekCode , '' ) />
227
228			<cfif (NOT find( '##' , rekCode ))
229				OR (NOT This.scanQoQ AND Variables.Regexes['isQueryOfQuery'].matches( QueryTagCode , 'partial' ) )
230				>
231				<cfcontinue />
232			</cfif>
233
234			<cfset var CurRow = QueryAddRow(qryResult)/>
235
236			<cfset qryResult.QueryCode[CurRow]    = QueryCode.replaceAll( Chr(13)&Chr(10) , Chr(10) ).replaceAll( Chr(13) , Chr(10) ) />
237			<cfset qryResult.FilteredCode[CurRow] = rekCode.replaceAll( Chr(13)&Chr(10) , Chr(10) ).replaceAll( Chr(13) , Chr(10) ) />
238
239			<cfif This.showScopeInfo >
240				<cfset var ScopesFound = {} />
241
242				<cfloop index="local.CurScope" array=#Variables.Regexes['findScopes'].match( rekCode )# >
243					<cfset ScopesFound[CurScope] = true />
244				</cfloop>
245
246				<cfset qryResult.ContainsClientScope[CurRow] = false />
247				<cfif This.highlightClientScopes>
248					<cfloop index="local.CurrentScope" array=#This.ClientScopes# >
249						<cfif StructKeyExists( ScopesFound , CurrentScope )>
250							<cfset qryResult.ContainsClientScope[CurRow] = true />
251							<cfbreak/>
252						</cfif>
253					</cfloop>
254				</cfif>
255
256				<cfset qryResult.ScopeList[CurRow] = StructKeyList(ScopesFound) />
257			</cfif>
258
259			<cfif This.ReturnSqlSegments AND findNoCase('select',ListFirst(qryResult.QueryCode[CurRow],chr(10))) >
260				<cftry>
261					<cfset var RawSegs = Regexes.Segs.match(qryResult.QueryCode[CurRow]) />
262					<cfset var SegStruct = {} />
263
264					<cfcatch type="java.lang.StackOverflowError">
265						<!---
266							There's a chance of failing on complex
267							queries; if so, ignore and continue scan.
268						--->
269						<cfset var RawSegs = [] />
270						<cfset var SegStruct = "failed" />
271					</cfcatch>
272				</cftry>
273
274				<cfloop index="local.CurSeg" array=#RawSegs# >
275					<cfset CurSeg = Variables.Regexes.SegNames.split( text=trim(CurSeg) , limit=1 ) />
276					<cfset CurSeg = {Name=CurSeg[1],Code=CurSeg[2]} />
277
278					<cfif StructKeyExists(SegStruct,CurSeg.Name)>
279						<cfif isSimpleValue(SegStruct[CurSeg.Name])>
280							<cfset SegStruct[CurSeg.Name] = [ SegStruct[CurSeg.Name] ] />
281						</cfif>
282						<cfset ArrayAppend(SegStruct[CurSeg.Name],trim(CurSeg.Code)) />
283					<cfelse>
284						<cfset SegStruct[CurSeg.Name] = trim(CurSeg.Code) />
285					</cfif>
286				</cfloop>
287
288				<cfset qryResult.QuerySegments[CurRow] = SegStruct />
289			</cfif>
290
291			<cfset var BeforeQueryCode = left( FileData , CurMatch.Pos ) />
292
293			<cfset var StartLine = 1+Variables.Regexes['Newline'].matches( BeforeQueryCode , 'count' ) />
294			<cfset var LineCount = Variables.Regexes['Newline'].matches( CurMatch.Match , 'count' ) />
295
296			<cfset qryResult.QueryStartLine[CurRow] = StartLine/>
297			<cfset qryResult.QueryEndLine[CurRow]   = StartLine + LineCount />
298			<cfset qryResult.QueryName[CurRow]      = ArrayToList(Variables.Regexes['findQueryName'].match(text=QueryTagCode,limit=1)) />
299			<cfset qryResult.QueryId[CurRow]        = hash(QueryTagCode&QueryCode,'SHA') />
300			<cfif NOT Len( qryResult.QueryName[CurRow] )>
301				<cfset qryResult.QueryName[CurRow] = "[unknown]"/>
302			</cfif>
303		</cfloop>
304
305		<cfset var CurFileId = hash( Arguments.FileName & hash(FileData,'SHA') , 'SHA' ) />
306		<cfloop query="qryResult">
307			<cfset qryResult.FileId[qryResult.CurrentRow]          = CurFileId />
308			<cfset qryResult.FileName[qryResult.CurrentRow]        = Arguments.FileName />
309			<cfset qryResult.QueryTotalCount[qryResult.CurrentRow] = ArrayLen(Matches) />
310			<cfset qryResult.QueryAlertCount[qryResult.CurrentRow] = qryResult.RecordCount />
311		</cfloop>
312		<cfset This.Totals.QueryCount += ArrayLen(Matches) />
313		<cfset This.Totals.AlertCount += qryResult.RecordCount />
314		<cfset This.Totals.FileCount++ />
315		<cfif qryResult.RecordCount >
316			<cfset This.Totals.RiskFileCount++ />
317		</cfif>
318
319		<cfreturn qryResult/>
320	</cffunction>
321
322
323	<cffunction name="QueryAppend" returntype="void" output=false access="private">
324		<cfargument name="QueryOne" type="Query" required_ />
325		<cfargument name="QueryTwo" type="Query" required_ />
326
327		<cfset var OrigRow = QueryOne.RecordCount />
328
329		<cfset QueryAddRow( QueryOne , QueryTwo.RecordCount )/>
330
331		<cfloop index="local.CurCol" list=#Variables.ResultFields# >
332			<cfloop query="QueryTwo">
333				<cfset QueryOne[CurCol][OrigRow+CurrentRow] = QueryTwo[CurCol][CurrentRow] />
334			</cfloop>
335		</cfloop>
336	</cffunction>
337
338
339</cfcomponent>