-
Notifications
You must be signed in to change notification settings - Fork 3.2k
Description
Existing issue checks
- I have searched existing issues
- Related: [BUG] fatal error: concurrent map writes in MultiPartForm.Decode during fuzzing #6947 (closed as NOT_PLANNED due to insufficient reproduction steps)
Current Behavior
When scanning multiple targets with a template that fuzzes multipart/form-data bodies, MultiPartForm.Decode triggers fatal error: concurrent map writes and the process crashes immediately.
This is a Go runtime fatal error that cannot be caught by recover().
Root Cause
MultiPartForm is the only stateful DataFormat implementation — it stores a boundary string field and a filesMetadata map field. However, it is registered as a singleton in dataformat.init() and returned by dataformat.Get() to every goroutine:
// pkg/fuzz/dataformat/dataformat.go
func init() {
dataformats = make(map[string]DataFormat)
RegisterDataFormat(NewMultiPartForm()) // singleton
}
func Get(name string) DataFormat {
return dataformats[name] // returns same instance to all callers
}When multiple targets are processed concurrently, each goroutine calls Body.parseBody("multipart/form-data", req) → dataformat.Get("multipart/form-data") → receives the same singleton → concurrent writes to m.boundary and m.filesMetadata:
Engine.executeTemplateWithTargets (spawns concurrent goroutines per target)
→ Engine.executeTemplateOnInput
→ TemplateExecuter.Execute
→ Request.ExecuteWithResults [request.go:518]
→ executeFuzzingRule → executeAllFuzzingRules [request_fuzz.go:68,155]
→ Rule.Execute [execute.go:107]
→ Body.Parse → parseBody("multipart/form-data") [body.go:68,83]
→ dataformat.Get("multipart/form-data") ← returns singleton
→ singleton.ParseBoundary(contentType) ← write to m.boundary (DATA RACE)
→ singleton.Decode(body) ← write to m.filesMetadata (fatal: concurrent map writes)
Why 1 target does NOT reproduce the issue
executeTemplateWithTargets (pkg/core/executors.go:95-118) creates an unbuffered channel and spawns worker goroutines. With 1 target, only 1 task enters the channel → only 1 worker goroutine processes it → no concurrent access to the singleton.
Rule.Execute (pkg/fuzz/execute.go:91-192) iterates over components and payloads sequentially within a single goroutine.
Therefore, the race condition only manifests with N > 1 targets, which is the primary use case of nuclei (scanning multiple targets).
Reproduction Steps
Prerequisites
- DAST mode enabled (SDK:
nuclei.DASTMode(), CLI:-dastflag) - Multiple targets (1 target = 1 goroutine = no concurrent access)
-raceflag for reliable detection (the race is non-deterministic; without-race, it may crash or silently corrupt data)
Template
Based on integration_tests/fuzz/fuzz-body-multipart-form-sqli.yaml:
id: multipart-fuzz
info:
name: multipart form body fuzzing
author: pdteam
severity: info
http:
- pre-condition:
- type: dsl
dsl:
- method != "GET"
- method != "HEAD"
- contains(content_type, "multipart/form-data")
condition: and
payloads:
injection:
- "'"
- "\""
- ";"
fuzzing:
- part: body
type: postfix
mode: single
fuzz:
- '{{injection}}'
stop-at-first-match: true
matchers:
- type: word
words:
- "ok"Self-contained SDK reproduction test
Copy-paste runnable test. Starts an httptest server, generates 20 multipart/form-data targets in proxify YAML format, and executes via the SDK.
package nuclei_test
import (
"context"
"fmt"
"net/http"
"net/http/httptest"
"net/url"
"os"
"testing"
nuclei "github.com/projectdiscovery/nuclei/v3/lib"
"github.com/projectdiscovery/nuclei/v3/pkg/output"
"github.com/stretchr/testify/require"
)
func TestMultiPartForm_ConcurrentMapWrites_SDK(t *testing.T) {
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(200)
fmt.Fprint(w, "ok")
}))
defer ts.Close()
parsedURL, err := url.Parse(ts.URL)
require.NoError(t, err)
host := parsedURL.Host
tmpTemplate, err := os.CreateTemp(t.TempDir(), "multipart-fuzz-*.yaml")
require.NoError(t, err)
_, err = tmpTemplate.WriteString(`id: multipart-fuzz
info:
name: multipart form body fuzzing
author: pdteam
severity: info
http:
- pre-condition:
- type: dsl
dsl:
- method != "GET"
- method != "HEAD"
- contains(content_type, "multipart/form-data")
condition: and
payloads:
injection:
- "'"
- "\""
- ";"
fuzzing:
- part: body
type: postfix
mode: single
fuzz:
- '{{injection}}'
stop-at-first-match: true
matchers:
- type: word
words:
- "ok"`)
require.NoError(t, err)
require.NoError(t, tmpTemplate.Close())
tmpInput, err := os.CreateTemp(t.TempDir(), "input-proxify-*.yaml")
require.NoError(t, err)
for i := range 20 {
_, err = fmt.Fprintf(tmpInput, `---
timestamp: 2024-01-01T00:00:00+00:00
url: %s/upload-%d
request:
header:
Content-Type: multipart/form-data; boundary=----WebKitFormBoundary7MA4YWxkTrZu0gW
method: POST
path: /upload-%d
host: %s
raw: |+
POST /upload-%d HTTP/1.1
Host: %s
Content-Type: multipart/form-data; boundary=----WebKitFormBoundary7MA4YWxkTrZu0gW
------WebKitFormBoundary7MA4YWxkTrZu0gW
Content-Disposition: form-data; name="file"; filename="test.txt"
Content-Type: text/plain
file content %d
------WebKitFormBoundary7MA4YWxkTrZu0gW
Content-Disposition: form-data; name="description"
test upload %d
------WebKitFormBoundary7MA4YWxkTrZu0gW--
response:
header:
Content-Type: text/plain
raw: |+
HTTP/1.1 200 OK
Content-Type: text/plain
ok
`, ts.URL, i, i, host, i, host, i, i)
require.NoError(t, err)
}
require.NoError(t, tmpInput.Close())
ne, err := nuclei.NewNucleiEngineCtx(
context.Background(),
nuclei.DASTMode(),
nuclei.WithTemplatesOrWorkflows(nuclei.TemplateSources{
Templates: []string{tmpTemplate.Name()},
}),
nuclei.DisableUpdateCheck(),
)
require.NoError(t, err)
defer ne.Close()
err = ne.LoadTargetsWithHttpData(tmpInput.Name(), "yaml")
require.NoError(t, err)
err = ne.ExecuteCallbackWithCtx(context.Background(), func(event *output.ResultEvent) {
fmt.Printf("Result: %s\n", event.TemplateID)
})
if err != nil {
t.Logf("ExecuteCallbackWithCtx error: %v", err)
}
}How to run
go test -race -v -count=1 -timeout=120s -run TestMultiPartForm_ConcurrentMapWrites_SDK ./lib/Output on dev branch (race detected)
==================
WARNING: DATA RACE
Write at 0x00c0002d0240 by goroutine 22936:
github.com/projectdiscovery/nuclei/v3/pkg/fuzz/dataformat.(*MultiPartForm).ParseBoundary()
pkg/fuzz/dataformat/multipart.go:146 +0x78
github.com/projectdiscovery/nuclei/v3/pkg/fuzz/component.(*Body).parseBody()
pkg/fuzz/component/body.go:83 +0xf8
github.com/projectdiscovery/nuclei/v3/pkg/fuzz/component.(*Body).Parse()
pkg/fuzz/component/body.go:68 +0x5bc
github.com/projectdiscovery/nuclei/v3/pkg/fuzz.(*Rule).Execute()
pkg/fuzz/execute.go:107 +0x5d8
github.com/projectdiscovery/nuclei/v3/pkg/protocols/http.(*Request).executeAllFuzzingRules()
pkg/protocols/http/request_fuzz.go:155 +0x62c
github.com/projectdiscovery/nuclei/v3/pkg/protocols/http.(*Request).executeFuzzingRule()
pkg/protocols/http/request_fuzz.go:68 +0x19c
github.com/projectdiscovery/nuclei/v3/pkg/protocols/http.(*Request).ExecuteWithResults()
pkg/protocols/http/request.go:518 +0x164
github.com/projectdiscovery/nuclei/v3/pkg/tmplexec/generic.(*Generic).ExecuteWithResults()
pkg/tmplexec/generic/exec.go:61 +0x404
github.com/projectdiscovery/nuclei/v3/pkg/tmplexec.(*TemplateExecuter).Execute()
pkg/tmplexec/exec.go:212 +0x5a4
github.com/projectdiscovery/nuclei/v3/pkg/core.(*Engine).executeTemplateOnInput()
pkg/core/executors.go:246 +0x214
github.com/projectdiscovery/nuclei/v3/pkg/core.(*Engine).executeTemplateWithTargets.func2.1()
pkg/core/executors.go:113 +0xd8
github.com/projectdiscovery/nuclei/v3/pkg/core.(*Engine).executeTemplateWithTargets.func2()
pkg/core/executors.go:118 +0xe0
Previous write at 0x00c0002d0240 by goroutine 22935:
[same stack — two goroutines writing to the same singleton concurrently]
--- FAIL: TestMultiPartForm_ConcurrentMapWrites_SDK (2.34s)
testing.go:1617: race detected during execution of test
FAIL
Production stack trace
fatal error: concurrent map writes
goroutine 896736 [running]:
github.com/projectdiscovery/nuclei/v3/pkg/fuzz/dataformat.(*MultiPartForm).Decode(...)
pkg/fuzz/dataformat/multipart.go:222 +0xbc7
github.com/projectdiscovery/nuclei/v3/pkg/fuzz/component.(*Body).parseBody(...)
pkg/fuzz/component/body.go:87 +0x108
github.com/projectdiscovery/nuclei/v3/pkg/fuzz/component.(*Body).Parse(...)
pkg/fuzz/component/body.go:68 +0x3d6
github.com/projectdiscovery/nuclei/v3/pkg/fuzz.(*Rule).Execute(...)
pkg/fuzz/execute.go:106 +0x371
Expected Behavior
Concurrent fuzzing of multipart/form-data bodies across multiple targets should work without crashing.
Environment
- nuclei: dev branch (d771daa)
- OS: macOS / Linux
- Go: 1.25.x