forked from BishopFox/jsluice
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathurl-match-xhr.go
169 lines (144 loc) · 4.44 KB
/
url-match-xhr.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
package jsluice
import (
"strings"
"sync"
"golang.org/x/exp/slices"
)
type nodeCache struct {
sync.RWMutex
data map[*Node][]*Node
}
func newNodeCache() *nodeCache {
return &nodeCache{
data: make(map[*Node][]*Node),
}
}
func (c *nodeCache) set(k *Node, v []*Node) {
c.Lock()
c.data[k] = v
c.Unlock()
}
func (c *nodeCache) get(k *Node) ([]*Node, bool) {
c.RLock()
v, exists := c.data[k]
c.RUnlock()
return v, exists
}
func matchXHR() URLMatcher {
cache := newNodeCache()
return URLMatcher{"call_expression", func(n *Node) *URL {
callName := n.ChildByFieldName("function").Content()
// We don't know what the XMLHttpRequest object will be called,
// so we have to focus on just the .open bit
if !strings.HasSuffix(callName, ".open") {
return nil
}
// There's a bunch of different stuff we might have matched,
// including window.open, so we're going to try and guess
// based on the first argument being a valid HTTP method.
// This will miss cases where the method is a variable.
arguments := n.ChildByFieldName("arguments")
method := arguments.NamedChild(0).RawString()
if !slices.Contains(
[]string{"GET", "HEAD", "OPTIONS", "POST", "PUT", "PATCH", "DELETE"},
method,
) {
return nil
}
urlArg := arguments.NamedChild(1)
if !urlArg.IsStringy() {
return nil
}
match := &URL{
URL: urlArg.CollapsedString(),
Method: method,
Type: "XMLHttpRequest.open",
Source: n.Content(),
}
// to find headers we need to look for calls to setRequestHeader() on
// the same object as the .open call. We'll stick to the same scope
// (i.e. sibling expressions) because we have no way to know if we're
// dealing with the same object or not otherwise.
objectName := strings.TrimSuffix(callName, ".open")
// We want to find the parent/ancestor node that defines the scope in which
// we are calling XHR.open(). JavaScript has three types of scope: global,
// function, and block. Block scope only comes into play if values
// are defined using 'let', or 'const'. We don't know if the XHR object
// was defined with let or const, so we're just going to ignore block scope.
// That leaves us with global scope and function scope. To find those we
// can ascend the tree until we hit a node with type "function_declaration",
// or we hit a nil parent.
parent := n.Parent()
if !parent.IsValid() {
return match
}
for {
candidate := parent.Parent()
if candidate == nil {
break
}
parent = candidate
pt := parent.Type()
if pt == "function_declaration" ||
pt == "function" ||
pt == "arrow_function" {
break
}
}
// Look for call_expressions under the same parent as our .open call.
// It's common to end up querying the exact same parent over and over
// again, so we cache the results on a per-parent node basis.
nodes := make([]*Node, 0)
if v, exists := cache.get(parent); exists {
nodes = v
} else {
q := `
(call_expression
function: (member_expression
object: (identifier)
property: (property_identifier)
)
arguments: (arguments (string))
) @matches
`
parent.Query(q, func(sibling *Node) {
nodes = append(nodes, sibling)
})
cache.set(parent, nodes)
}
headers := make(map[string]string, 0)
// TODO: I think we can get more accuracy here by relying on the fact that
// the .setRequestHeader calls we're interested in must come *after* the .open
// call in order to be valid. In theory that means we can skip any nodes at
// all that come before the .open call we're currently looking at. We could
// also stop looking after we see a .send call on the same object, although
// it's possible for the .send to be wrapped in a conditional so that might
// cause us to miss some values.
for _, sibling := range nodes {
name := sibling.ChildByFieldName("function").Content()
if !strings.HasSuffix(name, ".setRequestHeader") {
continue
}
if !strings.HasPrefix(name, objectName) {
continue
}
args := sibling.ChildByFieldName("arguments")
headerNode := args.NamedChild(0)
if headerNode == nil || headerNode.Type() != "string" {
continue
}
header := headerNode.RawString()
if _, exists := headers[header]; exists {
continue
}
var value string
valueNode := args.NamedChild(1)
if valueNode != nil && valueNode.Type() == "string" {
value = valueNode.RawString()
}
headers[header] = value
}
match.Headers = headers
return match
}}
}