- qpscanner/cfcs/qpscanner.cfc
- develop
- 12 KB
- 335
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>