- qpscanner/cfcs/qpscanner.cfc
- master
- 12 KB
- 338
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>