Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
projectdiscovery
GitHub Repository: projectdiscovery/nuclei
Path: blob/dev/pkg/protocols/http/request_test.go
2843 views
1
package http
2
3
import (
4
"context"
5
"fmt"
6
"net/http"
7
"net/http/httptest"
8
"sync/atomic"
9
"testing"
10
"time"
11
12
"github.com/stretchr/testify/require"
13
"github.com/tarunKoyalwar/goleak"
14
15
"github.com/projectdiscovery/nuclei/v3/pkg/model"
16
"github.com/projectdiscovery/nuclei/v3/pkg/model/types/severity"
17
"github.com/projectdiscovery/nuclei/v3/pkg/operators"
18
"github.com/projectdiscovery/nuclei/v3/pkg/operators/extractors"
19
"github.com/projectdiscovery/nuclei/v3/pkg/operators/matchers"
20
"github.com/projectdiscovery/nuclei/v3/pkg/output"
21
"github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/contextargs"
22
"github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/interactsh"
23
"github.com/projectdiscovery/nuclei/v3/pkg/testutils"
24
)
25
26
func TestHTTPExtractMultipleReuse(t *testing.T) {
27
options := testutils.DefaultOptions
28
29
testutils.Init(options)
30
templateID := "testing-http"
31
request := &Request{
32
ID: templateID,
33
Raw: []string{
34
`GET /robots.txt HTTP/1.1
35
Host: {{Hostname}}
36
User-Agent: Mozilla/5.0 (X11; Fedora; Linux x86_64; rv:80.0) Gecko/20100101 Firefox/80.0
37
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
38
Accept-Language: en-US,en;q=0.5
39
`,
40
41
`GET {{endpoint}} HTTP/1.1
42
Host: {{Hostname}}
43
User-Agent: Mozilla/5.0 (X11; Fedora; Linux x86_64; rv:80.0) Gecko/20100101 Firefox/80.0
44
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
45
Accept-Language: en-US,en;q=0.5
46
`,
47
},
48
Operators: operators.Operators{
49
Matchers: []*matchers.Matcher{{
50
Part: "body",
51
Type: matchers.MatcherTypeHolder{MatcherType: matchers.WordsMatcher},
52
Words: []string{"match /a", "match /b", "match /c"},
53
}},
54
Extractors: []*extractors.Extractor{{
55
Part: "body",
56
Name: "endpoint",
57
Type: extractors.ExtractorTypeHolder{ExtractorType: extractors.RegexExtractor},
58
Regex: []string{"(?m)/([a-zA-Z0-9-_/\\\\]+)"},
59
Internal: true,
60
}},
61
},
62
IterateAll: true,
63
}
64
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
65
switch r.URL.Path {
66
case "/robots.txt":
67
_, _ = fmt.Fprintf(w, `User-agent: Googlebot
68
Disallow: /a
69
Disallow: /b
70
Disallow: /c`)
71
default:
72
_, _ = fmt.Fprintf(w, `match %v`, r.URL.Path)
73
}
74
}))
75
defer ts.Close()
76
77
executerOpts := testutils.NewMockExecuterOptions(options, &testutils.TemplateInfo{
78
ID: templateID,
79
Info: model.Info{SeverityHolder: severity.Holder{Severity: severity.Low}, Name: "test"},
80
})
81
82
err := request.Compile(executerOpts)
83
require.Nil(t, err, "could not compile network request")
84
85
var finalEvent *output.InternalWrappedEvent
86
var matchCount int
87
t.Run("test", func(t *testing.T) {
88
metadata := make(output.InternalEvent)
89
previous := make(output.InternalEvent)
90
ctxArgs := contextargs.NewWithInput(context.Background(), ts.URL)
91
err := request.ExecuteWithResults(ctxArgs, metadata, previous, func(event *output.InternalWrappedEvent) {
92
if event.OperatorsResult != nil && event.OperatorsResult.Matched {
93
matchCount++
94
}
95
finalEvent = event
96
})
97
require.Nil(t, err, "could not execute network request")
98
})
99
require.NotNil(t, finalEvent, "could not get event output from request")
100
require.Equal(t, 3, matchCount, "could not get correct match count")
101
}
102
103
func TestDisableTE(t *testing.T) {
104
options := testutils.DefaultOptions
105
106
testutils.Init(options)
107
templateID := "http-disable-transfer-encoding"
108
109
// in raw request format
110
request := &Request{
111
ID: templateID,
112
Raw: []string{
113
`POST / HTTP/1.1
114
Host: {{Hostname}}
115
User-Agent: Mozilla/5.0 (X11; Fedora; Linux x86_64; rv:80.0) Gecko/20100101 Firefox/80.0
116
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
117
Accept-Language: en-US,en;q=0.5
118
119
login=1&username=admin&password=admin
120
`,
121
},
122
Operators: operators.Operators{
123
Matchers: []*matchers.Matcher{{
124
Type: matchers.MatcherTypeHolder{MatcherType: matchers.StatusMatcher},
125
Status: []int{200},
126
}},
127
},
128
}
129
130
// in base request format
131
request2 := &Request{
132
ID: templateID,
133
Method: HTTPMethodTypeHolder{MethodType: HTTPPost},
134
Path: []string{"{{BaseURL}}"},
135
Body: "login=1&username=admin&password=admin",
136
Operators: operators.Operators{
137
Matchers: []*matchers.Matcher{{
138
Type: matchers.MatcherTypeHolder{MatcherType: matchers.StatusMatcher},
139
Status: []int{200},
140
}},
141
},
142
}
143
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
144
if len(r.TransferEncoding) > 0 || r.ContentLength <= 0 {
145
t.Error("Transfer-Encoding header should not be set")
146
}
147
}))
148
defer ts.Close()
149
150
executerOpts := testutils.NewMockExecuterOptions(options, &testutils.TemplateInfo{
151
ID: templateID,
152
Info: model.Info{SeverityHolder: severity.Holder{Severity: severity.Low}, Name: "test"},
153
})
154
155
err := request.Compile(executerOpts)
156
require.Nil(t, err, "could not compile http raw request")
157
158
err = request2.Compile(executerOpts)
159
require.Nil(t, err, "could not compile http base request")
160
161
var finalEvent *output.InternalWrappedEvent
162
var matchCount int
163
t.Run("test", func(t *testing.T) {
164
metadata := make(output.InternalEvent)
165
previous := make(output.InternalEvent)
166
ctxArgs := contextargs.NewWithInput(context.Background(), ts.URL)
167
err := request.ExecuteWithResults(ctxArgs, metadata, previous, func(event *output.InternalWrappedEvent) {
168
if event.OperatorsResult != nil && event.OperatorsResult.Matched {
169
matchCount++
170
}
171
finalEvent = event
172
})
173
require.Nil(t, err, "could not execute network request")
174
})
175
176
t.Run("test2", func(t *testing.T) {
177
metadata := make(output.InternalEvent)
178
previous := make(output.InternalEvent)
179
ctxArgs := contextargs.NewWithInput(context.Background(), ts.URL)
180
err := request2.ExecuteWithResults(ctxArgs, metadata, previous, func(event *output.InternalWrappedEvent) {
181
if event.OperatorsResult != nil && event.OperatorsResult.Matched {
182
matchCount++
183
}
184
finalEvent = event
185
})
186
require.Nil(t, err, "could not execute network request")
187
})
188
189
require.NotNil(t, finalEvent, "could not get event output from request")
190
require.Equal(t, 2, matchCount, "could not get correct match count")
191
}
192
193
// consult @Ice3man543 before making any breaking changes to this test (context: vuln_hash)
194
func TestReqURLPattern(t *testing.T) {
195
options := testutils.DefaultOptions
196
197
// assume this was a preprocessor
198
// {{randstr}} => 2eNU2kbrOcUDzhnUL1RGvSo1it7
199
testutils.Init(options)
200
templateID := "testing-http"
201
request := &Request{
202
ID: templateID,
203
Raw: []string{
204
`GET /{{rand_char("abc")}}/{{interactsh-url}}/123?query={{rand_int(1, 10)}}&data=2eNU2kbrOcUDzhnUL1RGvSo1it7 HTTP/1.1
205
Host: {{Hostname}}
206
User-Agent: Mozilla/5.0 (X11; Fedora; Linux x86_64; rv:80.0) Gecko/20100101 Firefox/80.0
207
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
208
Accept-Language: en-US,en;q=0.5
209
`,
210
},
211
Operators: operators.Operators{
212
Matchers: []*matchers.Matcher{{
213
Type: matchers.MatcherTypeHolder{MatcherType: matchers.DSLMatcher},
214
DSL: []string{"true"},
215
}},
216
},
217
IterateAll: true,
218
}
219
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
220
// always return 200
221
w.WriteHeader(200)
222
_, _ = w.Write([]byte(`match`))
223
}))
224
defer ts.Close()
225
226
executerOpts := testutils.NewMockExecuterOptions(options, &testutils.TemplateInfo{
227
ID: templateID,
228
Info: model.Info{SeverityHolder: severity.Holder{Severity: severity.Low}, Name: "test"},
229
})
230
client, _ := interactsh.New(interactsh.DefaultOptions(executerOpts.Output, nil, executerOpts.Progress))
231
executerOpts.Interactsh = client
232
defer client.Close()
233
executerOpts.ExportReqURLPattern = true
234
235
// this is how generated constants are added to template
236
// generated constants are preprocessors that are executed while loading once
237
executerOpts.Constants = map[string]interface{}{
238
"{{randstr}}": "2eNU2kbrOcUDzhnUL1RGvSo1it7",
239
}
240
241
err := request.Compile(executerOpts)
242
require.Nil(t, err, "could not compile network request")
243
244
var finalEvent *output.InternalWrappedEvent
245
var matchCount int
246
t.Run("test", func(t *testing.T) {
247
metadata := make(output.InternalEvent)
248
previous := make(output.InternalEvent)
249
ctxArgs := contextargs.NewWithInput(context.Background(), ts.URL)
250
err := request.ExecuteWithResults(ctxArgs, metadata, previous, func(event *output.InternalWrappedEvent) {
251
if event.OperatorsResult != nil && event.OperatorsResult.Matched {
252
matchCount++
253
}
254
finalEvent = event
255
})
256
require.Nil(t, err, "could not execute network request")
257
})
258
require.NotNil(t, finalEvent, "could not get event output from request")
259
require.Equal(t, 1, matchCount, "could not get correct match count")
260
require.NotEmpty(t, finalEvent.Results[0].ReqURLPattern, "could not get req url pattern")
261
require.Equal(t, `/{{rand_char("abc")}}/{{interactsh-url}}/123?query={{rand_int(1, 10)}}&data={{randstr}}`, finalEvent.Results[0].ReqURLPattern)
262
}
263
264
// fakeHostErrorsCache implements hosterrorscache.CacheInterface minimally for tests
265
type fakeHostErrorsCache struct{}
266
267
func (f *fakeHostErrorsCache) SetVerbose(bool) {}
268
func (f *fakeHostErrorsCache) Close() {}
269
func (f *fakeHostErrorsCache) Remove(*contextargs.Context) {}
270
func (f *fakeHostErrorsCache) MarkFailed(string, *contextargs.Context, error) {}
271
func (f *fakeHostErrorsCache) MarkFailedOrRemove(string, *contextargs.Context, error) {
272
}
273
274
// Check always returns true to simulate an already unresponsive host
275
func (f *fakeHostErrorsCache) Check(string, *contextargs.Context) bool { return true }
276
277
// IsPermanentErr returns false for tests
278
func (f *fakeHostErrorsCache) IsPermanentErr(*contextargs.Context, error) bool { return false }
279
280
func TestExecuteParallelHTTP_StopAtFirstMatch(t *testing.T) {
281
options := testutils.DefaultOptions
282
testutils.Init(options)
283
284
// server that always matches
285
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
286
_, _ = fmt.Fprintf(w, "match")
287
}))
288
defer ts.Close()
289
290
templateID := "parallel-stop-first"
291
req := &Request{
292
ID: templateID,
293
Method: HTTPMethodTypeHolder{MethodType: HTTPGet},
294
Path: []string{"{{BaseURL}}/p?x={{v}}"},
295
Threads: 2,
296
Payloads: map[string]interface{}{
297
"v": []string{"1", "2"},
298
},
299
Operators: operators.Operators{
300
Matchers: []*matchers.Matcher{{
301
Part: "body",
302
Type: matchers.MatcherTypeHolder{MatcherType: matchers.WordsMatcher},
303
Words: []string{"match"},
304
}},
305
},
306
StopAtFirstMatch: true,
307
}
308
309
executerOpts := testutils.NewMockExecuterOptions(options, &testutils.TemplateInfo{
310
ID: templateID,
311
Info: model.Info{SeverityHolder: severity.Holder{Severity: severity.Low}, Name: "test"},
312
})
313
err := req.Compile(executerOpts)
314
require.NoError(t, err)
315
316
var matches int32
317
metadata := make(output.InternalEvent)
318
previous := make(output.InternalEvent)
319
ctxArgs := contextargs.NewWithInput(context.Background(), ts.URL)
320
err = req.ExecuteWithResults(ctxArgs, metadata, previous, func(event *output.InternalWrappedEvent) {
321
if event.OperatorsResult != nil && event.OperatorsResult.Matched {
322
atomic.AddInt32(&matches, 1)
323
}
324
})
325
require.NoError(t, err)
326
require.Equal(t, int32(1), atomic.LoadInt32(&matches), "expected only first match to be processed")
327
}
328
329
func TestExecuteParallelHTTP_SkipOnUnresponsiveFromCache(t *testing.T) {
330
options := testutils.DefaultOptions
331
testutils.Init(options)
332
333
// server that would match if reached
334
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
335
_, _ = fmt.Fprintf(w, "match")
336
}))
337
defer ts.Close()
338
339
templateID := "parallel-skip-unresponsive"
340
req := &Request{
341
ID: templateID,
342
Method: HTTPMethodTypeHolder{MethodType: HTTPGet},
343
Path: []string{"{{BaseURL}}/p?x={{v}}"},
344
Threads: 2,
345
Payloads: map[string]interface{}{
346
"v": []string{"1", "2"},
347
},
348
Operators: operators.Operators{
349
Matchers: []*matchers.Matcher{{
350
Part: "body",
351
Type: matchers.MatcherTypeHolder{MatcherType: matchers.WordsMatcher},
352
Words: []string{"match"},
353
}},
354
},
355
}
356
357
executerOpts := testutils.NewMockExecuterOptions(options, &testutils.TemplateInfo{
358
ID: templateID,
359
Info: model.Info{SeverityHolder: severity.Holder{Severity: severity.Low}, Name: "test"},
360
})
361
// inject fake host errors cache that forces skip
362
executerOpts.HostErrorsCache = &fakeHostErrorsCache{}
363
364
err := req.Compile(executerOpts)
365
require.NoError(t, err)
366
367
var matches int32
368
metadata := make(output.InternalEvent)
369
previous := make(output.InternalEvent)
370
ctxArgs := contextargs.NewWithInput(context.Background(), ts.URL)
371
err = req.ExecuteWithResults(ctxArgs, metadata, previous, func(event *output.InternalWrappedEvent) {
372
if event.OperatorsResult != nil && event.OperatorsResult.Matched {
373
atomic.AddInt32(&matches, 1)
374
}
375
})
376
require.NoError(t, err)
377
require.Equal(t, int32(0), atomic.LoadInt32(&matches), "expected no matches when host is marked unresponsive")
378
}
379
380
// TestExecuteParallelHTTP_GoroutineLeaks uses goleak to detect goroutine leaks in all HTTP parallel execution scenarios
381
func TestExecuteParallelHTTP_GoroutineLeaks(t *testing.T) {
382
defer goleak.VerifyNone(t,
383
goleak.IgnoreAnyContainingPkg("go.opencensus.io/stats/view"),
384
goleak.IgnoreAnyContainingPkg("github.com/syndtr/goleveldb"),
385
goleak.IgnoreAnyContainingPkg("github.com/go-rod/rod"),
386
goleak.IgnoreAnyContainingPkg("github.com/projectdiscovery/interactsh/pkg/server"),
387
goleak.IgnoreAnyContainingPkg("github.com/projectdiscovery/interactsh/pkg/client"),
388
goleak.IgnoreAnyContainingPkg("github.com/projectdiscovery/ratelimit"),
389
goleak.IgnoreAnyFunction("github.com/syndtr/goleveldb/leveldb/util.(*BufferPool).drain"),
390
goleak.IgnoreAnyFunction("github.com/syndtr/goleveldb/leveldb.(*DB).compactionError"),
391
goleak.IgnoreAnyFunction("github.com/syndtr/goleveldb/leveldb.(*DB).mpoolDrain"),
392
goleak.IgnoreAnyFunction("github.com/syndtr/goleveldb/leveldb.(*DB).tCompaction"),
393
goleak.IgnoreAnyFunction("github.com/syndtr/goleveldb/leveldb.(*DB).mCompaction"),
394
)
395
396
options := testutils.DefaultOptions
397
testutils.Init(options)
398
defer testutils.Cleanup(options)
399
400
// Test Case 1: Normal execution with StopAtFirstMatch
401
t.Run("StopAtFirstMatch", func(t *testing.T) {
402
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
403
time.Sleep(10 * time.Millisecond)
404
_, _ = fmt.Fprintf(w, "test response")
405
}))
406
defer ts.Close()
407
408
req := &Request{
409
ID: "parallel-stop-first-match",
410
Method: HTTPMethodTypeHolder{MethodType: HTTPGet},
411
Path: []string{"{{BaseURL}}/test?param={{payload}}"},
412
Threads: 4,
413
Payloads: map[string]interface{}{
414
"payload": []string{"1", "2", "3", "4", "5", "6", "7", "8"},
415
},
416
Operators: operators.Operators{
417
Matchers: []*matchers.Matcher{{
418
Part: "body",
419
Type: matchers.MatcherTypeHolder{MatcherType: matchers.WordsMatcher},
420
Words: []string{"test response"},
421
}},
422
},
423
StopAtFirstMatch: true,
424
}
425
426
executerOpts := testutils.NewMockExecuterOptions(options, &testutils.TemplateInfo{
427
ID: "parallel-stop-first-match",
428
Info: model.Info{SeverityHolder: severity.Holder{Severity: severity.Low}, Name: "test"},
429
})
430
431
err := req.Compile(executerOpts)
432
require.NoError(t, err)
433
434
metadata := make(output.InternalEvent)
435
previous := make(output.InternalEvent)
436
ctxArgs := contextargs.NewWithInput(context.Background(), ts.URL)
437
438
err = req.ExecuteWithResults(ctxArgs, metadata, previous, func(event *output.InternalWrappedEvent) {})
439
require.NoError(t, err)
440
})
441
442
// Test Case 2: Unresponsive host scenario
443
t.Run("UnresponsiveHost", func(t *testing.T) {
444
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
445
_, _ = fmt.Fprintf(w, "response")
446
}))
447
defer ts.Close()
448
449
req := &Request{
450
ID: "parallel-unresponsive",
451
Method: HTTPMethodTypeHolder{MethodType: HTTPGet},
452
Path: []string{"{{BaseURL}}/test?param={{payload}}"},
453
Threads: 3,
454
Payloads: map[string]interface{}{
455
"payload": []string{"1", "2", "3", "4", "5"},
456
},
457
Operators: operators.Operators{
458
Matchers: []*matchers.Matcher{{
459
Part: "body",
460
Type: matchers.MatcherTypeHolder{MatcherType: matchers.WordsMatcher},
461
Words: []string{"response"},
462
}},
463
},
464
}
465
466
executerOpts := testutils.NewMockExecuterOptions(options, &testutils.TemplateInfo{
467
ID: "parallel-unresponsive",
468
Info: model.Info{SeverityHolder: severity.Holder{Severity: severity.Low}, Name: "test"},
469
})
470
executerOpts.HostErrorsCache = &fakeHostErrorsCache{}
471
472
err := req.Compile(executerOpts)
473
require.NoError(t, err)
474
475
metadata := make(output.InternalEvent)
476
previous := make(output.InternalEvent)
477
ctxArgs := contextargs.NewWithInput(context.Background(), ts.URL)
478
479
err = req.ExecuteWithResults(ctxArgs, metadata, previous, func(event *output.InternalWrappedEvent) {})
480
require.NoError(t, err)
481
})
482
483
// Test Case 3: Context cancellation scenario
484
t.Run("ContextCancellation", func(t *testing.T) {
485
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
486
time.Sleep(200 * time.Millisecond)
487
_, _ = fmt.Fprintf(w, "response")
488
}))
489
defer ts.Close()
490
491
req := &Request{
492
ID: "parallel-context-cancel",
493
Method: HTTPMethodTypeHolder{MethodType: HTTPGet},
494
Path: []string{"{{BaseURL}}/test?param={{payload}}"},
495
Threads: 3,
496
Payloads: map[string]interface{}{
497
"payload": []string{"1", "2", "3", "4", "5"},
498
},
499
Operators: operators.Operators{
500
Matchers: []*matchers.Matcher{{
501
Part: "body",
502
Type: matchers.MatcherTypeHolder{MatcherType: matchers.WordsMatcher},
503
Words: []string{"response"},
504
}},
505
},
506
}
507
508
executerOpts := testutils.NewMockExecuterOptions(options, &testutils.TemplateInfo{
509
ID: "parallel-context-cancel",
510
Info: model.Info{SeverityHolder: severity.Holder{Severity: severity.Low}, Name: "test"},
511
})
512
513
err := req.Compile(executerOpts)
514
require.NoError(t, err)
515
516
metadata := make(output.InternalEvent)
517
previous := make(output.InternalEvent)
518
519
ctx, cancel := context.WithCancel(context.Background())
520
ctxArgs := contextargs.NewWithInput(ctx, ts.URL)
521
522
go func() {
523
time.Sleep(50 * time.Millisecond)
524
cancel()
525
}()
526
527
err = req.ExecuteWithResults(ctxArgs, metadata, previous, func(event *output.InternalWrappedEvent) {})
528
require.Error(t, err)
529
require.Equal(t, context.Canceled, err)
530
})
531
}
532
533