Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
projectdiscovery
GitHub Repository: projectdiscovery/nuclei
Path: blob/dev/pkg/fuzz/analyzers/time/time_delay_test.go
2070 views
1
// Tests ported from ZAP Java version of the algorithm
2
3
package time
4
5
import (
6
"math/rand"
7
"reflect"
8
"testing"
9
"time"
10
)
11
12
// This test suite verifies the timing dependency detection algorithm by testing various scenarios:
13
//
14
// Test Categories:
15
// 1. Perfect Linear Cases
16
// - TestPerfectLinear: Basic case with slope=1, no noise
17
// - TestPerfectLinearSlopeOne_NoNoise: Similar to above but with different parameters
18
// - TestPerfectLinearSlopeTwo_NoNoise: Tests detection of slope=2 relationship
19
//
20
// 2. Noisy Cases
21
// - TestLinearWithNoise: Verifies detection works with moderate noise (±0.2s)
22
// - TestNoisyLinear: Similar but with different noise parameters
23
// - TestHighNoiseConcealsSlope: Verifies detection fails with extreme noise (±5s)
24
//
25
// 3. No Correlation Cases
26
// - TestNoCorrelation: Basic case where delay has no effect
27
// - TestNoCorrelationHighBaseline: High baseline (~15s) masks any delay effect
28
// - TestNegativeSlopeScenario: Verifies detection rejects negative correlations
29
//
30
// 4. Edge Cases
31
// - TestMinimalData: Tests behavior with minimal data points (2 requests)
32
// - TestLargeNumberOfRequests: Tests stability with many data points (20 requests)
33
// - TestChangingBaseline: Tests detection with shifting baseline mid-test
34
// - TestHighBaselineLowSlope: Tests detection of subtle correlations (slope=0.85)
35
//
36
// ZAP Test Cases:
37
//
38
// 1. Alternating Sequence Tests
39
// - TestAlternatingSequences: Verifies correct alternation between high and low delays
40
//
41
// 2. Non-Injectable Cases
42
// - TestNonInjectableQuickFail: Tests quick failure when response time < requested delay
43
// - TestSlowNonInjectableCase: Tests early termination with consistently high response times
44
// - TestRealWorldNonInjectableCase: Tests behavior with real-world response patterns
45
//
46
// 3. Error Tolerance Tests
47
// - TestSmallErrorDependence: Verifies detection works with small random variations
48
//
49
// Key Parameters Tested:
50
// - requestsLimit: Number of requests to make (2-20)
51
// - highSleepTimeSeconds: Maximum delay to test (typically 5s)
52
// - correlationErrorRange: Acceptable deviation from perfect correlation (0.05-0.3)
53
// - slopeErrorRange: Acceptable deviation from expected slope (0.1-1.5)
54
//
55
// The test suite uses various mock senders (perfectLinearSender, noCorrelationSender, etc.)
56
// to simulate different timing behaviors and verify the detection algorithm works correctly
57
// across a wide range of scenarios.
58
59
// Mock request sender that simulates a perfect linear relationship:
60
// Observed delay = baseline + requested_delay
61
func perfectLinearSender(baseline float64) func(delay int) (float64, error) {
62
return func(delay int) (float64, error) {
63
// simulate some processing time
64
time.Sleep(10 * time.Millisecond) // just a small artificial sleep to mimic network
65
return baseline + float64(delay), nil
66
}
67
}
68
69
// Mock request sender that simulates no correlation:
70
// The response time is random around a certain constant baseline, ignoring requested delay.
71
func noCorrelationSender(baseline, noiseAmplitude float64) func(int) (float64, error) {
72
return func(delay int) (float64, error) {
73
time.Sleep(10 * time.Millisecond)
74
noise := 0.0
75
if noiseAmplitude > 0 {
76
noise = (rand.Float64()*2 - 1) * noiseAmplitude
77
}
78
return baseline + noise, nil
79
}
80
}
81
82
// Mock request sender that simulates partial linearity but with some noise.
83
func noisyLinearSender(baseline float64) func(delay int) (float64, error) {
84
return func(delay int) (float64, error) {
85
time.Sleep(10 * time.Millisecond)
86
// Add some noise (±0.2s) to a linear relationship
87
noise := 0.2
88
return baseline + float64(delay) + noise, nil
89
}
90
}
91
92
func TestPerfectLinear(t *testing.T) {
93
// Expect near-perfect correlation and slope ~ 1.0
94
requestsLimit := 6 // 3 pairs: enough data for stable regression
95
highSleepTimeSeconds := 5
96
corrErrRange := 0.1
97
slopeErrRange := 0.2
98
baseline := 5.0
99
100
sender := perfectLinearSender(5.0) // baseline 5s, observed = 5s + requested_delay
101
match, reason, err := checkTimingDependency(
102
requestsLimit,
103
highSleepTimeSeconds,
104
corrErrRange,
105
slopeErrRange,
106
baseline,
107
sender,
108
)
109
if err != nil {
110
t.Fatalf("Unexpected error: %v", err)
111
}
112
if !match {
113
t.Fatalf("Expected a match but got none. Reason: %s", reason)
114
}
115
}
116
117
func TestNoCorrelation(t *testing.T) {
118
// Expect no match because requested delay doesn't influence observed delay
119
requestsLimit := 6
120
highSleepTimeSeconds := 5
121
corrErrRange := 0.1
122
slopeErrRange := 0.5
123
baseline := 8.0
124
125
sender := noCorrelationSender(8.0, 0.1)
126
match, reason, err := checkTimingDependency(
127
requestsLimit,
128
highSleepTimeSeconds,
129
corrErrRange,
130
slopeErrRange,
131
baseline,
132
sender,
133
)
134
if err != nil {
135
t.Fatalf("Unexpected error: %v", err)
136
}
137
if match {
138
t.Fatalf("Expected no match but got one. Reason: %s", reason)
139
}
140
}
141
142
func TestNoisyLinear(t *testing.T) {
143
// Even with some noise, it should detect a strong positive correlation if
144
// we allow a slightly bigger margin for slope/correlation.
145
requestsLimit := 10 // More requests to average out noise
146
highSleepTimeSeconds := 5
147
corrErrRange := 0.2 // allow some lower correlation due to noise
148
slopeErrRange := 0.5 // slope may deviate slightly
149
baseline := 2.0
150
151
sender := noisyLinearSender(2.0) // baseline 2s, observed ~ 2s + requested_delay ±0.2
152
match, reason, err := checkTimingDependency(
153
requestsLimit,
154
highSleepTimeSeconds,
155
corrErrRange,
156
slopeErrRange,
157
baseline,
158
sender,
159
)
160
if err != nil {
161
t.Fatalf("Unexpected error: %v", err)
162
}
163
164
// We expect a match since it's still roughly linear. The slope should be close to 1.
165
if !match {
166
t.Fatalf("Expected a match in noisy linear test but got none. Reason: %s", reason)
167
}
168
}
169
170
func TestMinimalData(t *testing.T) {
171
// With too few requests, correlation might not be stable.
172
// Here, we send only 2 requests (1 pair) and see if the logic handles it gracefully.
173
requestsLimit := 2
174
highSleepTimeSeconds := 5
175
corrErrRange := 0.3
176
slopeErrRange := 0.5
177
baseline := 5.0
178
179
// Perfect linear sender again
180
sender := perfectLinearSender(5.0)
181
match, reason, err := checkTimingDependency(
182
requestsLimit,
183
highSleepTimeSeconds,
184
corrErrRange,
185
slopeErrRange,
186
baseline,
187
sender,
188
)
189
if err != nil {
190
t.Fatalf("Unexpected error: %v", err)
191
}
192
if !match {
193
t.Fatalf("Expected match but got none. Reason: %s", reason)
194
}
195
}
196
197
// Utility functions to generate different behaviors
198
199
// linearSender returns a sender that calculates observed delay as:
200
// observed = baseline + slope * requested_delay + noise
201
func linearSender(baseline, slope, noiseAmplitude float64) func(int) (float64, error) {
202
return func(delay int) (float64, error) {
203
time.Sleep(10 * time.Millisecond)
204
noise := 0.0
205
if noiseAmplitude > 0 {
206
noise = (rand.Float64()*2 - 1) * noiseAmplitude // random noise in [-noiseAmplitude, noiseAmplitude]
207
}
208
return baseline + slope*float64(delay) + noise, nil
209
}
210
}
211
212
// negativeSlopeSender just for completeness - higher delay = less observed time
213
func negativeSlopeSender(baseline float64) func(int) (float64, error) {
214
return func(delay int) (float64, error) {
215
time.Sleep(10 * time.Millisecond)
216
return baseline - float64(delay)*2.0, nil
217
}
218
}
219
220
func TestPerfectLinearSlopeOne_NoNoise(t *testing.T) {
221
baseline := 2.0
222
match, reason, err := checkTimingDependency(
223
10, // requestsLimit
224
5, // highSleepTimeSeconds
225
0.1, // correlationErrorRange
226
0.2, // slopeErrorRange (allowing slope between 0.8 and 1.2)
227
baseline,
228
linearSender(baseline, 1.0, 0.0),
229
)
230
if err != nil {
231
t.Fatalf("Unexpected error: %v", err)
232
}
233
if !match {
234
t.Fatalf("Expected a match for perfect linear slope=1. Reason: %s", reason)
235
}
236
}
237
238
func TestPerfectLinearSlopeTwo_NoNoise(t *testing.T) {
239
baseline := 2.0
240
// slope=2 means observed = baseline + 2*requested_delay
241
match, reason, err := checkTimingDependency(
242
10,
243
5,
244
0.1, // correlation must still be good
245
1.5, // allow slope in range (0.5 to 2.5), we should be close to 2.0 anyway
246
baseline,
247
linearSender(baseline, 2.0, 0.0),
248
)
249
if err != nil {
250
t.Fatalf("Error: %v", err)
251
}
252
if !match {
253
t.Fatalf("Expected a match for slope=2. Reason: %s", reason)
254
}
255
}
256
257
func TestLinearWithNoise(t *testing.T) {
258
baseline := 5.0
259
// slope=1 but with noise ±0.2 seconds
260
match, reason, err := checkTimingDependency(
261
12,
262
5,
263
0.2, // correlationErrorRange relaxed to account for noise
264
0.5, // slopeErrorRange also relaxed
265
baseline,
266
linearSender(baseline, 1.0, 0.2),
267
)
268
if err != nil {
269
t.Fatalf("Error: %v", err)
270
}
271
if !match {
272
t.Fatalf("Expected a match for noisy linear data. Reason: %s", reason)
273
}
274
}
275
276
func TestNoCorrelationHighBaseline(t *testing.T) {
277
baseline := 15.0
278
// baseline ~15s, requested delays won't matter
279
match, reason, err := checkTimingDependency(
280
10,
281
5,
282
0.1, // correlation should be near zero, so no match expected
283
0.5,
284
baseline,
285
noCorrelationSender(baseline, 0.1),
286
)
287
if err != nil {
288
t.Fatalf("Error: %v", err)
289
}
290
if match {
291
t.Fatalf("Expected no match for no correlation scenario. Got: %s", reason)
292
}
293
}
294
295
func TestNegativeSlopeScenario(t *testing.T) {
296
baseline := 10.0
297
// Increasing delay decreases observed time
298
match, reason, err := checkTimingDependency(
299
10,
300
5,
301
0.2,
302
0.5,
303
baseline,
304
negativeSlopeSender(baseline),
305
)
306
if err != nil {
307
t.Fatalf("Error: %v", err)
308
}
309
if match {
310
t.Fatalf("Expected no match in negative slope scenario. Reason: %s", reason)
311
}
312
}
313
314
func TestLargeNumberOfRequests(t *testing.T) {
315
baseline := 1.0
316
// 20 requests, slope=1.0, no noise. Should be very stable and produce a very high correlation.
317
match, reason, err := checkTimingDependency(
318
20,
319
5,
320
0.05, // very strict correlation requirement
321
0.1, // very strict slope range
322
baseline,
323
linearSender(baseline, 1.0, 0.0),
324
)
325
if err != nil {
326
t.Fatalf("Error: %v", err)
327
}
328
if !match {
329
t.Fatalf("Expected a strong match with many requests and perfect linearity. Reason: %s", reason)
330
}
331
}
332
333
func TestHighBaselineLowSlope(t *testing.T) {
334
baseline := 15.0
335
match, reason, err := checkTimingDependency(
336
10,
337
5,
338
0.2,
339
0.2, // expecting slope around 0.5, allow range ~0.4 to 0.6
340
baseline,
341
linearSender(baseline, 0.85, 0.0),
342
)
343
if err != nil {
344
t.Fatalf("Error: %v", err)
345
}
346
if !match {
347
t.Fatalf("Expected a match for slope=0.5 linear scenario. Reason: %s", reason)
348
}
349
}
350
351
func TestHighNoiseConcealsSlope(t *testing.T) {
352
baseline := 5.0
353
// slope=1, but noise=5 seconds is huge and might conceal the correlation.
354
// With large noise, the test may fail to detect correlation.
355
match, reason, err := checkTimingDependency(
356
12,
357
5,
358
0.1, // still strict
359
0.2, // still strict
360
baseline,
361
linearSender(baseline, 1.0, 5.0),
362
)
363
if err != nil {
364
t.Fatalf("Error: %v", err)
365
}
366
// Expect no match because the noise level is too high to establish a reliable correlation.
367
if match {
368
t.Fatalf("Expected no match due to extreme noise. Reason: %s", reason)
369
}
370
}
371
372
func TestAlternatingSequences(t *testing.T) {
373
baseline := 0.0
374
var generatedDelays []float64
375
reqSender := func(delay int) (float64, error) {
376
generatedDelays = append(generatedDelays, float64(delay))
377
return float64(delay), nil
378
}
379
match, reason, err := checkTimingDependency(
380
4, // requestsLimit
381
15, // highSleepTimeSeconds
382
0.1, // correlationErrorRange
383
0.2, // slopeErrorRange
384
baseline,
385
reqSender,
386
)
387
if err != nil {
388
t.Fatalf("Unexpected error: %v", err)
389
}
390
if !match {
391
t.Fatalf("Expected a match but got none. Reason: %s", reason)
392
}
393
// Verify alternating sequence of delays
394
expectedDelays := []float64{15, 3, 15, 3}
395
if !reflect.DeepEqual(generatedDelays, expectedDelays) {
396
t.Fatalf("Expected delays %v but got %v", expectedDelays, generatedDelays)
397
}
398
}
399
400
func TestNonInjectableQuickFail(t *testing.T) {
401
baseline := 0.5
402
var timesCalled int
403
reqSender := func(delay int) (float64, error) {
404
timesCalled++
405
return 0.5, nil // Return value less than delay
406
}
407
match, _, err := checkTimingDependency(
408
4, // requestsLimit
409
15, // highSleepTimeSeconds
410
0.1, // correlationErrorRange
411
0.2, // slopeErrorRange
412
baseline,
413
reqSender,
414
)
415
if err != nil {
416
t.Fatalf("Unexpected error: %v", err)
417
}
418
if match {
419
t.Fatal("Expected no match for non-injectable case")
420
}
421
if timesCalled != 1 {
422
t.Fatalf("Expected quick fail after 1 call, got %d calls", timesCalled)
423
}
424
}
425
426
func TestSlowNonInjectableCase(t *testing.T) {
427
baseline := 10.0
428
rng := rand.New(rand.NewSource(time.Now().UnixNano()))
429
var timesCalled int
430
reqSender := func(delay int) (float64, error) {
431
timesCalled++
432
return 10 + rng.Float64()*0.5, nil
433
}
434
match, _, err := checkTimingDependency(
435
4, // requestsLimit
436
15, // highSleepTimeSeconds
437
0.1, // correlationErrorRange
438
0.2, // slopeErrorRange
439
baseline,
440
reqSender,
441
)
442
if err != nil {
443
t.Fatalf("Unexpected error: %v", err)
444
}
445
if match {
446
t.Fatal("Expected no match for slow non-injectable case")
447
}
448
if timesCalled > 3 {
449
t.Fatalf("Expected early termination (≤3 calls), got %d calls", timesCalled)
450
}
451
}
452
453
func TestRealWorldNonInjectableCase(t *testing.T) {
454
baseline := 0.0
455
var iteration int
456
counts := []float64{11, 21, 11, 21, 11}
457
reqSender := func(delay int) (float64, error) {
458
iteration++
459
return counts[iteration-1], nil
460
}
461
match, _, err := checkTimingDependency(
462
4, // requestsLimit
463
15, // highSleepTimeSeconds
464
0.1, // correlationErrorRange
465
0.2, // slopeErrorRange
466
baseline,
467
reqSender,
468
)
469
if err != nil {
470
t.Fatalf("Unexpected error: %v", err)
471
}
472
if match {
473
t.Fatal("Expected no match for real-world non-injectable case")
474
}
475
if iteration > 4 {
476
t.Fatalf("Expected ≤4 iterations, got %d", iteration)
477
}
478
}
479
480
func TestSmallErrorDependence(t *testing.T) {
481
baseline := 0.0
482
rng := rand.New(rand.NewSource(time.Now().UnixNano()))
483
reqSender := func(delay int) (float64, error) {
484
return float64(delay) + rng.Float64()*0.5, nil
485
}
486
match, reason, err := checkTimingDependency(
487
4, // requestsLimit
488
15, // highSleepTimeSeconds
489
0.1, // correlationErrorRange
490
0.2, // slopeErrorRange
491
baseline,
492
reqSender,
493
)
494
if err != nil {
495
t.Fatalf("Unexpected error: %v", err)
496
}
497
if !match {
498
t.Fatalf("Expected match for small error case. Reason: %s", reason)
499
}
500
}
501
502