Sorcerer's IsleCode QueryParam Scanner / files

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