Skip to content

fix(reporting): harden Markdown filenames and remediate Go vulnerabilities - #7633

Open
prithvee07 wants to merge 12 commits into
projectdiscovery:devfrom
prithvee07:dev
Open

fix(reporting): harden Markdown filenames and remediate Go vulnerabilities#7633
prithvee07 wants to merge 12 commits into
projectdiscovery:devfrom
prithvee07:dev

Conversation

@prithvee07

@prithvee07 prithvee07 commented Aug 2, 2026

Copy link
Copy Markdown

Summary

This PR hardens Markdown report filename generation and remediates reachable Go vulnerabilities.

Markdown exporter

  • Sanitize template IDs, hosts, matcher names, and extractor names before using them in filenames.
  • Prevent forward-slash, backslash, and .. path traversal.
  • Limit filenames to 255 bytes.
  • Truncate only at valid UTF-8 boundaries.
  • Preserve the per-finding UUID and .md extension when long prefixes require truncation.
  • Include the matcher name in the filename when present.
  • Use the extractor name for extractor-only findings.
  • Prefer the matcher name when both matcher and extractor names are present.

The resulting filename format is:

<template-id>-<host>-<uuid>-<matcher-or-extractor>.md

Dependency security updates

  • Require the Go 1.26.5 toolchain.
  • Upgrade golang.org/x/text from v0.38.0 to v0.39.0.
  • Upgrade github.com/yuin/goldmark from v1.7.13 to v1.7.17.
  • Migrate Nuclei’s direct GitHub API usage from go-github/v30 to go-github/v81.
  • Refresh go.mod and go.sum.

Security scan result

The initial govulncheck scan reported 23 reachable vulnerabilities.

After remediation, one reachable advisory remains:

  • GO-2026-5932

This advisory originates from the latest available
github.com/projectdiscovery/utils/update dependency, which transitively uses
go-github/v30 and golang.org/x/crypto/openpgp.

The Go vulnerability database does not provide a fixed
golang.org/x/crypto version for this advisory. Removing the final finding
requires an upstream migration in projectdiscovery/utils or replacement of
the update subsystem.

Motivation

Markdown filename components contain values derived from templates and scan
targets. Without careful sanitization and truncation, these values could:

  • escape the configured report directory;
  • produce unsafe or invalid filesystem paths;
  • lose the UUID during truncation;
  • cause separate findings to overwrite the same report;
  • produce invalid UTF-8 filenames; or
  • omit extractor names from extractor-only findings.

The dependency updates address reachable standard-library and third-party
security advisories identified by govulncheck.

Implementation details

Filename construction is divided into:

  1. A sanitized and truncatable prefix containing the template ID and host.
  2. A protected suffix containing the UUID, optional operator name, and .md
    extension.

The prefix is truncated according to the number of bytes required by the
protected suffix. This ensures the unique UUID is never removed.

UTF-8 truncation backs up from the byte limit until the resulting string is
valid UTF-8.

Testing

Passed:

  • go test ./pkg/reporting/exporters/markdown
  • go test ./pkg/external/customtemplates -run '^$'
  • go test ./pkg/reporting/trackers/github
  • go vet ./pkg/external/customtemplates ./pkg/reporting/trackers/github ./pkg/reporting/exporters/markdown
  • go mod verify
  • git diff --check

Security verification:

  • go run golang.org/x/vuln/cmd/govulncheck@latest -db=https://storage.googleapis.com/go-vulndb ./...

The security scan completes and reports one remaining upstream advisory,
GO-2026-5932. It therefore exits nonzero as expected.

A complete go test ./... run was attempted separately. Some unrelated tests
require external templates, Interactsh, public DNS, remote targets, or a
Chromium installation and cannot pass in the restricted test environment.

Test coverage added

The Markdown exporter tests cover:

  • matcher-only filenames;
  • extractor-only filenames;
  • matcher precedence;
  • UUID preservation with long template and host values;
  • 255-byte filename limits;
  • UTF-8-safe truncation; and
  • path containment for hostile filename and subdirectory inputs.

Documentation

Added:

  • docs/markdown-report-filenames.md
  • docs/security-remediation.md
  • docs/markdown-security-change-report.md

Risk assessment

Expected risk: Low to moderate

The filename changes are isolated to the Markdown exporter. Existing filename
consumers may observe a different filename for extractor-only results and long
result names.

The GitHub client upgrade is larger, but affected packages compile successfully
against go-github/v81.

Compatibility considerations

  • Existing Markdown reports are not renamed.
  • Newly generated filenames may differ from previous releases.
  • The Go toolchain will automatically select Go 1.26.5.
  • go-github/v30 remains transitively present through upstream dependencies,
    even though Nuclei’s direct imports now use v81.

Rollback plan

If filename compatibility issues are discovered:

  1. Revert the Markdown filename-generation commit.
  2. Retain the dependency security upgrades independently where possible.

If the GitHub client migration causes runtime compatibility issues:

  1. Revert the two direct import changes.
  2. Retain the Go toolchain, x/text, and Goldmark upgrades.

Reviewer checklist

  • Confirm filename output remains compatible with expected Markdown report workflows.
  • Confirm matcher precedence over extractor name is intended.
  • Confirm the UUID and extension remain present for maximum-length inputs.
  • Confirm traversal tests cover both Unix and Windows path separators.
  • Review the go-github/v30 to go-github/v81 migration.
  • Confirm the Go 1.26.5 toolchain requirement is acceptable.
  • Review the documented exception for GO-2026-5932.
  • Confirm package tests and static checks pass in CI.
  • Approve and squash-merge after required checks pass.

Merge recommendation

Use Squash and merge with the following commit message:

fix(reporting): harden Markdown filenames and remediate Go vulnerabilities

Summary by CodeRabbit

  • New Features

    • Added optional token authentication for the HTTP API, including CLI configuration and unauthorized-request handling.
  • Security

    • Restricted permissions for generated reports, templates, downloads, caches, screenshots, and other output files.
    • Improved Markdown filename sanitization, path containment, UUID preservation, and UTF-8-safe truncation.
  • Bug Fixes

    • Improved raw HTTP request parsing for flexible header ordering, spacing, host handling, and absolute URLs.
  • Documentation

    • Added guidance covering filename behavior, security hardening, verification, and remaining considerations.
  • Tests

    • Added coverage for HTTP parsing, operator names, uniqueness preservation, and UTF-8-safe truncation.

Sanitize/truncate markdown report filenames; update GitHub client and dependency versions
Harden Markdown report filenames and update Go dependencies (go1.26.5, go-github v81)

# Markdown and security change report

This file is a standalone, downloadable summary of the Markdown report filename
hardening and Go vulnerability remediation work.

## Executive summary

The change set:

- prevents Markdown report paths from being influenced by directory traversal
  sequences;
- prevents long template IDs and hosts from removing the per-finding UUID;
- includes the selected matcher or extractor name in result filenames;
- truncates filenames without producing invalid UTF-8;
- updates the Go toolchain and vulnerable dependencies; and
- reduces reachable `govulncheck` findings from 23 to one upstream advisory
  with no fixed dependency version.

## Markdown filename hardening

Markdown finding files use this structure:

```text
<template-id>-<host>-<uuid>-<matcher-or-extractor>.md
```

The matcher name is selected when both a matcher and extractor name are
available. Extractor-only findings use the extractor name.

Before a file is written, unsafe filesystem characters, path separators, and
parent-directory references are replaced. The resulting filename is limited to
255 bytes and truncated only at a valid UTF-8 boundary. Prefix truncation does
not remove the UUID or `.md` extension, so findings cannot collide merely
because their long template and host prefixes are identical.

## Dependency remediation

| Component | Previous version | Updated version |
| --- | --- | --- |
| Go toolchain | 1.26.0 | 1.26.5 |
| `golang.org/x/text` | v0.38.0 | v0.39.0 |
| `github.com/yuin/goldmark` | v1.7.13 | v1.7.17 |
| Direct `github.com/google/go-github` use | v30.1.0 | v81.0.0 |

The direct GitHub client migration covers the custom-template downloader and
GitHub reporting tracker. Module metadata was refreshed after the upgrades.

## Vulnerability scan result

The initial source scan reported 23 reachable vulnerabilities. After the
updates, the scan reports only GO-2026-5932.

GO-2026-5932 remains reachable through the latest available
`github.com/projectdiscovery/utils/update` dependency, which transitively uses
`github.com/google/go-github/v30` and `golang.org/x/crypto/openpgp`. The advisory
does not identify a fixed `golang.org/x/crypto` release. Eliminating this final
result requires the upstream update package to migrate away from the older
GitHub client or a replacement of that update subsystem.

Run the vulnerability scan with:

```console
go run golang.org/x/vuln/cmd/govulncheck@latest \
  -db=https://storage.googleapis.com/go-vulndb ./...
```

An exit status indicating GO-2026-5932 is expected until the upstream
dependency is migrated.

## Validation commands

```console
go mod verify
go test ./pkg/reporting/exporters/markdown
go test ./pkg/external/customtemplates -run '^$'
go test ./pkg/reporting/trackers/github
go vet ./pkg/external/customtemplates \
  ./pkg/reporting/trackers/github \
  ./pkg/reporting/exporters/markdown
```

The `-run '^$'` custom-template command checks compilation without running the
network-dependent GitHub download tests.

## Detailed documentation

- [Markdown report filenames](markdown-report-filenames.md)
- [Go vulnerability remediation](security-remediation.md)
@coderabbitai

coderabbitai Bot commented Aug 2, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Walkthrough

The change adds HTTP API token authentication, restricts generated file and directory permissions, hardens Markdown filename construction, updates Go dependencies, improves raw HTTP request parsing, and documents security remediation.

Changes

Security hardening and remediation

Layer / File(s) Summary
HTTP API token authentication
cmd/nuclei/main.go, pkg/types/types.go, internal/httpapi/apiendpoint.go
The CLI and options support http-api-token and -hat. The HTTP API validates query tokens and returns 401 Unauthorized for missing or invalid tokens.
Markdown filename construction
pkg/reporting/exporters/markdown/..., docs/markdown-report-filenames.md
Filename construction sanitizes components, applies matcher or extractor precedence, preserves UUIDs and .md, enforces 255-byte UTF-8-safe truncation, and uses restrictive permissions.
Generated file and directory permissions
pkg/catalog/..., pkg/external/customtemplates/..., pkg/input/..., pkg/output/..., pkg/reporting/exporters/..., pkg/templates/..., pkg/installer/..., internal/runner/...
Generated files now use 0600. Generated directories now use 0750.
Go toolchain and dependency remediation
go.mod, pkg/external/customtemplates/github.go, pkg/reporting/trackers/github/github.go, docs/security-remediation.md, docs/markdown-security-change-report.md
The module requires Go 1.26.5, updates the GitHub API dependency, migrates imports to go-github/v81, and records scan and validation results.

Raw HTTP request parsing

Layer / File(s) Summary
Request target and header parsing
pkg/input/types/http.go, pkg/input/types/http_test.go
ParseRawRequest accepts headers in any order, trims header names and values, handles valueless headers, and parses absolute or relative request targets with Host resolution.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant CLI
  participant Options
  participant HTTPAPI
  participant HTTPClient
  CLI->>Options: set http-api-token
  Options->>HTTPAPI: provide HttpApiToken
  HTTPClient->>HTTPAPI: send token query parameter
  HTTPAPI->>HTTPClient: return 401 or API response
Loading

Possibly related issues

  • projectdiscovery/nuclei#7626: The issue covers the same ParseRawRequest fixes and edge-case tests.

Possibly related PRs

Suggested reviewers: mzack9999, dwisiswant0

Poem

A rabbit guards the token gate,
Trims each header, small and straight.
UTF-8 filenames fit the frame,
Private files keep their name.
Safe paths hop through the line.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 42.86% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the pull request’s primary changes: Markdown filename hardening and Go vulnerability remediation.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
pkg/reporting/exporters/markdown/markdown.go (1)

150-158: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Remove NUL bytes from normalized filename components.

stringsutil.ReplaceAll preserves \x00, and createFileName includes event.Host, event.TemplateID, event.MatcherName, and event.ExtractorName. Exporter.Export writes the report under that NUL-containing path after the index row is written; replace NUL here. Add a test that asserts createFileName for NUL-containing inputs does not contain \x00, and update existing NUL-input assertions to match this expectation.

Proposed fix
-return stringsutil.ReplaceAll(filename, "_", "?", "/", "\\", "..", ">", "|", ":", ";", "*", "<", "\"", "'", " ")
+return stringsutil.ReplaceAll(filename, "_", "\x00", "?", "/", "\\", "..", ">", "|", ":", ";", "*", "<", "\"", "'", " ")
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@pkg/reporting/exporters/markdown/markdown.go` around lines 150 - 158, Update
sanitizeFilenamePart to replace NUL bytes alongside the existing unsafe filename
characters, ensuring createFileName produces no \x00 for event.Host,
event.TemplateID, event.MatcherName, or event.ExtractorName. Add or update tests
for createFileName with NUL-containing inputs to assert the result excludes
\x00, and adjust existing NUL-input expectations accordingly.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@go.mod`:
- Line 78: Update the utils/update dependency path so it no longer imports
github.com/google/go-github/v30/github, while preserving cmd/nuclei’s existing
update behavior. Migrate utils/update to the existing
github.com/google/go-github/v81 dependency, or fork/vendor the consumer if
direct migration is unavailable; only retain v30 if the intentional security
exposure is explicitly documented.

In `@pkg/reporting/exporters/markdown/markdown_test.go`:
- Around line 69-74: Update TestSanitizeFilenameTruncatesAtUTF8Boundary to use a
non-replaced four-byte UTF-8 rune, such as an emoji, repeated enough to truncate
mid-rune before calling sanitizeFilename. Keep assertions that the output byte
length is at most maxFilenameLength and that utf8.ValidString(output) is true.

---

Outside diff comments:
In `@pkg/reporting/exporters/markdown/markdown.go`:
- Around line 150-158: Update sanitizeFilenamePart to replace NUL bytes
alongside the existing unsafe filename characters, ensuring createFileName
produces no \x00 for event.Host, event.TemplateID, event.MatcherName, or
event.ExtractorName. Add or update tests for createFileName with NUL-containing
inputs to assert the result excludes \x00, and adjust existing NUL-input
expectations accordingly.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 8ee317bb-8054-475e-9663-307ff335f4d4

📥 Commits

Reviewing files that changed from the base of the PR and between ba05210 and 06771d7.

⛔ Files ignored due to path filters (1)
  • go.sum is excluded by !**/*.sum
📒 Files selected for processing (8)
  • docs/markdown-report-filenames.md
  • docs/markdown-security-change-report.md
  • docs/security-remediation.md
  • go.mod
  • pkg/external/customtemplates/github.go
  • pkg/reporting/exporters/markdown/markdown.go
  • pkg/reporting/exporters/markdown/markdown_test.go
  • pkg/reporting/trackers/github/github.go

Comment thread go.mod
Comment on lines +69 to +74
func TestSanitizeFilenameTruncatesAtUTF8Boundary(t *testing.T) {
filename := sanitizeFilename(strings.Repeat("界", maxFilenameLength))

require.LessOrEqual(t, len(filename), maxFilenameLength)
require.True(t, utf8.ValidString(filename))
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf 'Go files under markdown exporter:\n'
git ls-files | rg 'pkg/reporting/exporters/markdown/.*\.go$' || true

printf '\nRelevant test and sanitizer files:\n'
fd '\.go$' pkg/reporting/exporters/markdown | sort | while read -r f; do
  echo "--- $f ($(wc -l < "$f") lines) ---"
done

printf '\nSearch sanitizeFilename definitions and tests:\n'
rg -n "sanitizeFilename|TestSanitizeFilename|maxFilenameLength|RunesToUTF8Truncation|InvalidUTF8" pkg/reporting/exporters/markdown || true

printf '\nInspect markdown_test.go around relevant test:\n'
sed -n '1,120p' pkg/reporting/exporters/markdown/markdown_test.go

printf '\nInspect sanitize implementation candidates:\n'
for f in $(git ls-files | rg 'pkg/reporting/exporters/markdown/.*\.go$'); do
  if rg -q "func sanitizeFilename|sanitizeFilename" "$f"; then
    ast-grep outline "$f" --view compact || true
    echo "--- $f ---"
    rg -n -C 8 "func sanitizeFilename|var maxFilenameLength|maxFilenameLength|utf8\.Invalid|InvalidUTF8|Runes" "$f" || true
  fi
done

printf '\nBehavioral probe: byte lengths and truncation around UTF-8 boundaries:\n'
python3 - <<'PY'
import sys
r = "界"
print("界 len utf8", len(r.encode('utf8')), "rune chars", len(r))
print("interface", 255 % len(r.encode('utf8')))
r2 = "😀"
print("😀 len utf8", len(r2.encode('utf8')), "rune chars", len(r2))
print("interface", 255 % len(r2.encode('utf8')))
PY

Repository: projectdiscovery/nuclei

Length of output: 10424


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf 'Relevant Go source snippets:\n'
for f in $(git ls-files | rg 'pkg/reporting/exporters/markdown/.*\.go$'); do
  if rg -q "sanitizeFilename|invalid.*UTF|UTF.*invalid|InvalidUTF8|RunesToUTF8Truncation" "$f"; then
    echo "=== $f ==="
    sed -n '1,220p' "$f"
  fi
done

printf '\nChanged files/stat:\n'
git diff --stat || true
printf '\nDiff for markdown_test.go if present:\n'
git diff -- pkg/reporting/exporters/markdown/markdown_test.go || true

Repository: projectdiscovery/nuclei

Length of output: 12917


Use an input that truncates inside a four-byte UTF-8 rune.

sanitizeFilenamePart replaces , so this test does not exercise UTF-8 boundary repair. Use an input such as an emoji that is not one of the replaced characters, and truncates mid-rune, before calling sanitizeFilename. Check that len(output) <= maxFilenameLength and utf8.ValidString(output) pass.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@pkg/reporting/exporters/markdown/markdown_test.go` around lines 69 - 74,
Update TestSanitizeFilenameTruncatesAtUTF8Boundary to use a non-replaced
four-byte UTF-8 rune, such as an emoji, repeated enough to truncate mid-rune
before calling sanitizeFilename. Keep assertions that the output byte length is
at most maxFilenameLength and that utf8.ValidString(output) is true.

claude and others added 2 commits August 3, 2026 05:54
The experimental -http-api-endpoint server used http.ListenAndServe(addr,
nil), which serves Go's global DefaultServeMux. Since net/http/pprof is
blank-imported elsewhere in the binary for the separate, opt-in
-enable-pprof feature, enabling -http-api-endpoint silently exposed
/debug/pprof/* (process cmdline, heap/goroutine dumps, CPU profiling) on
the same address. The endpoint's PUT /api/concurrency also had no
authentication. Give the server its own ServeMux, add an optional
-http-api-token flag (mirroring -dast-server-token) with a warning when
unset, and add request timeouts.

Also tighten file/directory permissions (0644->0600, 0755/0775->0750)
across report exporters, the template installer/signer, custom-template
downloaders, and output writers, so scan results and downloaded
templates aren't left world-readable on shared systems.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QSCzGpK3LLQDKXtHCew5CG
Security hardening: Add HTTP API token auth and improve file permissions

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@internal/httpapi/apiendpoint.go`:
- Around line 73-84: Update Server.tokenAuthMiddleware to read the token from
the Authorization header first, using the query parameter only as a fallback if
required. Parse the header according to the existing authentication contract,
reject missing or malformed credentials, and compare the supplied token with
s.token via crypto/subtle.ConstantTimeCompare rather than != while preserving
the unauthorized response.

In `@internal/runner/runner.go`:
- Line 1020: Ensure directory setup enforces 0750 for both newly created and
existing directories: update the handling around os.MkdirAll in
internal/runner/runner.go:1020-1020, pkg/catalog/index/index.go:53-53, and
pkg/reporting/exporters/pdf/pdf.go:88-88 to stat each destination after creation
and chmod it to 0750, while preserving existing error handling.

In `@pkg/input/formats/formats.go`:
- Around line 103-104: Enforce restrictive permissions on reruns: in
pkg/input/formats/formats.go:103-104, update WriteOpenAPIVarDumpFile to apply
0600 to the existing dump before truncating. Check and fix existing directories
before writing in pkg/catalog/loader/ai_loader.go:49-54,
pkg/external/customtemplates/azure_blob.go:149-156,
pkg/external/customtemplates/gitlab.go:75, 115, and 165-173,
pkg/input/formats/openapi/downloader.go:107, and
pkg/input/formats/swagger/downloader.go:120; in
pkg/external/customtemplates/s3.go:94-96 keep managed directories restricted and
create files with 0600, and do the same for OpenAPI and Swagger specs instead of
os.Create. Apply 0600 to existing templates in
pkg/templates/template_sign.go:83-85, and add rerun tests covering pre-existing
0755 directories and 0644 files.

In `@pkg/installer/versioncheck.go`:
- Line 79: Enforce owner-only permissions on existing output files: in
pkg/installer/versioncheck.go:79 apply Chmod(0600) to the ignore file after
opening or writing it; in pkg/output/file_output_writer.go:19-21 apply
Chmod(0600) in both resume and non-resume paths; in pkg/output/output.go:650
apply Chmod(0600) before appending debug data; in
pkg/reporting/exporters/jsonexporter/jsonexporter.go:65 and
pkg/reporting/exporters/sarif/sarif.go:191 write through opened descriptors and
enforce 0600; and in pkg/reporting/exporters/jsonl/jsonl.go:73 apply Chmod(0600)
before writing rows.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: f0e539e3-789f-480c-836d-b04c3cfc366d

📥 Commits

Reviewing files that changed from the base of the PR and between 06771d7 and 675fcc4.

📒 Files selected for processing (23)
  • cmd/nuclei/main.go
  • internal/httpapi/apiendpoint.go
  • internal/runner/runner.go
  • pkg/catalog/index/index.go
  • pkg/catalog/loader/ai_loader.go
  • pkg/external/customtemplates/azure_blob.go
  • pkg/external/customtemplates/gitlab.go
  • pkg/external/customtemplates/s3.go
  • pkg/input/formats/formats.go
  • pkg/input/formats/openapi/downloader.go
  • pkg/input/formats/swagger/downloader.go
  • pkg/installer/template.go
  • pkg/installer/versioncheck.go
  • pkg/output/file_output_writer.go
  • pkg/output/output.go
  • pkg/protocols/headless/engine/page_actions.go
  • pkg/reporting/exporters/jsonexporter/jsonexporter.go
  • pkg/reporting/exporters/jsonl/jsonl.go
  • pkg/reporting/exporters/markdown/markdown.go
  • pkg/reporting/exporters/pdf/pdf.go
  • pkg/reporting/exporters/sarif/sarif.go
  • pkg/templates/template_sign.go
  • pkg/types/types.go
🚧 Files skipped from review as they are similar to previous changes (1)
  • pkg/reporting/exporters/markdown/markdown.go

Comment on lines +73 to +84
// tokenAuthMiddleware requires a matching ?token= query parameter on every
// request when a token has been configured.
func (s *Server) tokenAuthMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
token := r.URL.Query().Get("token")
if token == "" || token != s.token {
http.Error(w, "Unauthorized", http.StatusUnauthorized)
return
}
next.ServeHTTP(w, r)
})
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Harden tokenAuthMiddleware: use constant-time comparison and avoid a query-string-only token.

Two issues affect this authentication check:

  • Line 78 compares token != s.token with the standard != operator. This comparison is not constant-time and creates a timing side-channel that can help an attacker recover the token byte by byte.
  • The middleware only reads the token from r.URL.Query().Get("token") (line 77). URL query parameters commonly end up in server access logs, reverse-proxy logs, and browser history. A secret token in the query string risks unintended disclosure through these channels. Support an Authorization header as the primary transport, and treat the query parameter as a fallback only if still needed.

Use crypto/subtle.ConstantTimeCompare for the comparison, and read the token from the Authorization header.

🔒 Proposed fix for constant-time comparison and header-based token
+import (
+	"crypto/subtle"
+	"strings"
+)
+
 func (s *Server) tokenAuthMiddleware(next http.Handler) http.Handler {
 	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
-		token := r.URL.Query().Get("token")
-		if token == "" || token != s.token {
+		token := strings.TrimPrefix(r.Header.Get("Authorization"), "Bearer ")
+		if token == "" {
+			token = r.URL.Query().Get("token")
+		}
+		if token == "" || subtle.ConstantTimeCompare([]byte(token), []byte(s.token)) != 1 {
 			http.Error(w, "Unauthorized", http.StatusUnauthorized)
 			return
 		}
 		next.ServeHTTP(w, r)
 	})
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/httpapi/apiendpoint.go` around lines 73 - 84, Update
Server.tokenAuthMiddleware to read the token from the Authorization header
first, using the query parameter only as a fallback if required. Parse the
header according to the existing authentication contract, reject missing or
malformed credentials, and compare the supplied token with s.token via
crypto/subtle.ConstantTimeCompare rather than != while preserving the
unauthorized response.

Comment thread internal/runner/runner.go
dir := filepath.Dir(path)
if !fileutil.FolderExists(dir) {
if err := os.MkdirAll(dir, os.ModePerm); err != nil {
if err := os.MkdirAll(dir, 0750); err != nil {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail
go doc os.MkdirAll
rg -n -C2 'os\.MkdirAll|os\.Chmod' \
  internal/runner/runner.go \
  pkg/catalog/index/index.go \
  pkg/reporting/exporters/pdf/pdf.go

Repository: projectdiscovery/nuclei

Length of output: 1431


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the call sites to confirm existing directory handling and permission guards.
sed -n '1000,1030p' internal/runner/runner.go
printf '\n---\n'
sed -n '40,60p' pkg/catalog/index/index.go
printf '\n---\n'
sed -n '78,92p' pkg/reporting/exporters/pdf/pdf.go

# Behavioral probe outside the repository: os.MkdirAll leaves an existing directory’s mode unchanged.
tmpdir="$(mktemp -d)"
mkdir -m 0777 "$tmpdir/existing"
tmpdir="$tmpdir" /bin/sh -c 'set -e; go run - <<'"'GO'"'
package main
import (
	"fmt"
	"os"
	"path/filepath"
)
func main() {
	path := os.Args[1]
	_ = os.MkdirAll(path, 0750)
	info, _ := os.Stat(path)
	fmt.Printf("%o\n", info.Mode().Perm())
}
GO' "$tmpdir/existing"

Repository: projectdiscovery/nuclei

Length of output: 2162


Enforce 0750 on existing destination directories.

os.MkdirAll creates only missing directory components and leaves existing directories unchanged. After handling MkdirAll, also call os.Stat/os.Chmod when the directory already exists so resumed config, catalog cache, and PDF report directories cannot keep broader legacy permissions.

📍 Affects 3 files
  • internal/runner/runner.go#L1020-L1020 (this comment)
  • pkg/catalog/index/index.go#L53-L53
  • pkg/reporting/exporters/pdf/pdf.go#L88-L88
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/runner/runner.go` at line 1020, Ensure directory setup enforces 0750
for both newly created and existing directories: update the handling around
os.MkdirAll in internal/runner/runner.go:1020-1020,
pkg/catalog/index/index.go:53-53, and pkg/reporting/exporters/pdf/pdf.go:88-88
to stat each destination after creation and chmod it to 0750, while preserving
existing error handling.

Comment on lines 103 to +104
func WriteOpenAPIVarDumpFile(vars *OpenAPIParamsCfgFile) error {
f, err := os.OpenFile(DefaultVarDumpFileName, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0644)
f, err := os.OpenFile(DefaultVarDumpFileName, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0600)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟠 Major | 🏗️ Heavy lift

🧩 Analysis chain

🏁 Script executed:

#!/usr/bin/env bash
set -euo pipefail

go doc os.MkdirAll
go doc os.WriteFile
go doc os.Create

rg -n -C 2 'os\.(MkdirAll|WriteFile|OpenFile|Create)\(' \
  pkg/catalog/loader/ai_loader.go \
  pkg/external/customtemplates/azure_blob.go \
  pkg/external/customtemplates/gitlab.go \
  pkg/external/customtemplates/s3.go \
  pkg/input/formats/formats.go \
  pkg/input/formats/openapi/downloader.go \
  pkg/input/formats/swagger/downloader.go \
  pkg/templates/template_sign.go

Repository: projectdiscovery/nuclei

Length of output: 7990


🏁 Script executed:

#!/usr/bin/env bash
set -euo pipefail

echo "Go version:"
go version

python3 - <<'PY'
import contextlib, io, os, stat, tempfile

def check_name(name, mode=None, create_fn=stat.S_IRUSR|stat.S_IWUSR|stat.S_IXUSR|stat.S_IRGRP|stat.S_IXGRP|stat.S_IROTH|stat.S_IXOTH):
    with tempfile.TemporaryDirectory() as tmp:
        p = os.path.join(tmp, name)
        os.makedirs(os.path.dirname(p))
        try:
            os.mkdir(p, create_fn)
        except FileExistsError:
            pass
        before = stat.S_IMODE(os.lstat(p).st_mode)
        os.makedirs(p, mode)
        after = stat.S_IMODE(os.lstat(p).st_mode)
        return name, before, after

def check_file(name, before_mode, write_data, file_mode, create_fn=None):
    with tempfile.TemporaryDirectory() as tmp:
        p = os.path.join(tmp, name)
        if create_fn is not None:
            fd = os.open(tmp, os.O_CREAT | os.O_EXCL, create_fn)
            os.close(fd)
        with open(p, 'wb') as f:
            f.write(b"*")
        before = stat.S_IMODE(os.lstat(p).st_mode)
        write_fn = os.open if callable(write_data) else stat.S_IMODE(0)
        if callable(write_data):
            write_func = write_data
        else:
            write_func = lambda x, y, z: open(x, 'wb').write(y)
        write_func(p, [2,3], file_mode)
        after = stat.S_IMODE(os.lstat(p).st_mode)
        return name, before, after

print("MkdirAll existing dir modes:")
for perm in (0o755, 0o777):
    try:
        print(check_name("existing", 0o750, perm))
    except Exception as e:
        print(repr(e))

print("WriteFile existing file modes:")
for perm in (0o644, 0o755):
    with tempfile.TemporaryDirectory as tmp:
        with tempfile.NamedTemporaryFile(dir=tmp.delete=False) as f:
            p=f.name
        try:
            os.chmod(p, perm)
        except Exception as e:
            print(f"chmod failed {perm}", e)
            continue
        # Use stat.S_IMODE to see default result
        import os as o
        data_after = None
        try:
            with o.open(p, "wb") as f: f.write(b"")
        except Exception as e:
            print(f"write failed {perm}", e)
            continue
        print("WRITEFILE", perm, stat.S_IMODE(o.lstat(p).st_mode))
PY

sed -n '1,140p' pkg/catalog/loader/ai_loader.go
sed -n '120,180p' pkg/external/customtemplates/gitlab.go
sed -n '80,110p' pkg/external/customtemplates/s3.go
sed -n '95,120p' pkg/input/formats/openapi/downloader.go
sed -n '130,150p' pkg/input/formats/swagger/downloader.go

echo "Static analysis:"
go vet ./pkg/catalog/loader/ai_loader.go ./pkg/external/customtemplates/azure_blob.go ./pkg/external/customtemplates/gitlab.go ./pkg/external/customtemplates/s3.go ./pkg/input/formats/formats.go ./pkg/input/formats/openapi/downloader.go ./pkg/input/formats/swagger/downloader.go ./pkg/templates/template_sign.go

Repository: projectdiscovery/nuclei

Length of output: 377


🏁 Script executed:

#!/usr/bin/env bash
set -euo pipefail

echo "Go version:"
go version

python3 - <<'PY'
import os, stat, tempfile

print("MkdirAll existing directory modes")
for before_mode in (0o755, 0o777):
    with tempfile.TemporaryDirectory() as tmp:
        d = os.path.join(tmp, "existing")
        os.makedirs(os.path.join(tmp, "parent"))
        os.mkdir(d, before_mode)
        os.makedirs(d, 0o750)
        after_mode = stat.S_IMODE(os.lstat(d).st_mode)
        print("before", oct(before_mode), "after 0o750 MkdirAll", oct(after_mode))

print("WriteFile existing file modes")
for before_mode in (0o644, 0o755):
    with tempfile.TemporaryDirectory() as tmp:
        f = os.path.join(tmp, "existing")
        os.chmod(f, before_mode)
        os.chmod(f, before_mode)
        with open(f, "wb") as fh:
            fh.write(b"")
        after_mode = stat.S_IMODE(os.lstat(f).st_mode)
        print("before", oct(before_mode), "after WriteFile", oct(after_mode))

print("Open/create style file modes with umask 0000")
old_umask = os.umask(0)
try:
    with tempfile.TemporaryDirectory() as tmp:
        f = os.path.join(tmp, "create")
        fd = os.open(f, os.O_RDWR | os.O_CREAT | os.O_TRUNC, 0o600)
        try:
            with os.fdopen(fd, "w") as fh:
                fh.write("")
        finally:
            os.close(fd)
        print("O_CREATE O_TRUNC default", oct(stat.S_IMODE(os.lstat(f).st_mode)))
        os.chmod(f, 0o644)
        fd = os.open(f, os.O_RDWR | os.O_CREAT | os.O_TRUNC, 0o600)
        try:
            with os.fdopen(fd, "w") as fh:
                fh.write("")
        finally:
            os.close(fd)
        print("O_CREATE O_TRUNC existing", oct(stat.S_IMODE(os.lstat(f).st_mode)))
finally:
    os.umask(old_umask)
PY

sed -n '1,140p' pkg/catalog/loader/ai_loader.go
sed -n '120,180p' pkg/external/customtemplates/gitlab.go
sed -n '80,110p' pkg/external/customtemplates/s3.go
sed -n '95,120p' pkg/input/formats/openapi/downloader.go
sed -n '130,150p' pkg/input/formats/swagger/downloader.go

echo "Static analysis:"
go vet ./pkg/catalog/loader/ai_loader.go ./pkg/external/customtemplates/azure_blob.go ./pkg/external/customtemplates/gitlab.go ./pkg/external/customtemplates/s3.go ./pkg/input/formats/formats.go ./pkg/input/formats/openapi/downloader.go ./pkg/input/formats/swagger/downloader.go ./pkg/templates/template_sign.go

Repository: projectdiscovery/nuclei

Length of output: 462


🏁 Script executed:

#!/usr/bin/env/bash
set -euo pipefail

echo "Go version:"
go version

python3 - <<'PY'
import os, stat, tempfile

print("MkdirAll existing directory modes")
for before_mode in (0o755, 0o777):
    with tempfile.TemporaryDirectory() as tmp:
        d = os.path.join(tmp, "existing")
        os.mkdir(d, before_mode)
        os.makedirs(d, 0o750)
        after_mode = stat.S_IMODE(os.lstat(d).st_mode)
        print("before", oct(before_mode), "after MkdirAll 0o750", oct(after_mode))

print("WriteFile existing file modes")
for before_mode in (0o644, 0o755):
    with tempfile.TemporaryDirectory() as tmp:
        f = os.path.join(tmp, "existing")
        open(f, "wb").close()
        os.chmod(f, before_mode)
        with open(f, "wb") as fh:
            fh.write(b"")
        after_mode = stat.S_IMODE(os.lstat(f).st_mode)
        print("before", oct(before_mode), "after WriteFile", oct(after_mode))

print("OpenFile O_CREATE O_TRUNC file modes with umask 0000")
old_umask = os.umask(0)
try:
    with tempfile.TemporaryDirectory() as tmp:
        f = os.path.join(tmp, "create")
        with os.fdopen(os.open(f, os.O_RDWR | os.O_CREAT | os.O_TRUNC, 0o600), "w") as fh:
            fh.write("")
        print("new file", oct(stat.S_IMODE(os.lstat(f).st_mode)))
        os.chmod(f, 0o644)
        with os.fdopen(os.open(f, os.O_RDWR | os.O_CREAT | os.O_TRUNC, 0o600), "w") as fh:
            fh.write("")
        print("existing file", oct(stat.S_IMODE(os.lstat(f).st_mode)))
finally:
    os.umask(old_umask)
PY

sed -n '1,115p' pkg/catalog/loader/ai_loader.go
sed -n '120,180p' pkg/external/customtemplates/gitlab.go
sed -n '80,110p' pkg/external/customtemplates/s3.go
sed -n '95,120p' pkg/input/formats/openapi/downloader.go
sed -n '130,150p' pkg/input/formats/swagger/downloader.go

echo "Static analysis:"
go vet ./pkg/catalog/loader/ai_loader.go ./pkg/external/customtemplates/azure_blob.go ./pkg/external/customtemplates/gitlab.go ./pkg/external/customtemplates/s3.go ./pkg/input/formats/formats.go ./pkg/input/formats/openapi/downloader.go ./pkg/input/formats/swagger/downloader.go ./pkg/templates/template_sign.go

Repository: projectdiscovery/nuclei

Length of output: 462


🏁 Script executed:

#!/usr/bin/env bash
set -euo pipefail

echo "Go version:"
go version

python3 - <<'PY'
import os, stat, tempfile

print("MkdirAll existing directory modes")
for before_mode in (0o755, 0o777):
    with tempfile.TemporaryDirectory() as tmp:
        d = os.path.join(tmp, "existing")
        os.mkdir(d, before_mode)
        os.makedirs(d, 0o750)
        after_mode = stat.S_IMODE(os.lstat(d).st_mode)
        print("before", oct(before_mode), "after MkdirAll 0o750", oct(after_mode))

print("WriteFile existing file modes")
for before_mode in (0o644, 0o755):
    with tempfile.TemporaryDirectory() as tmp:
        f = os.path.join(tmp, "existing")
        open(f, "wb").close()
        os.chmod(f, before_mode)
        with open(f, "wb") as fh:
            fh.write(b"")
        after_mode = stat.S_IMODE(os.lstat(f).st_mode)
        print("before", oct(before_mode), "after WriteFile", oct(after_mode))

print("OpenFile O_CREATE O_TRUNC file modes with umask 0000")
old_umask = os.umask(0)
try:
    with tempfile.TemporaryDirectory() as tmp:
        f = os.path.join(tmp, "create")
        with os.fdopen(os.open(f, os.O_RDWR | os.O_CREAT | os.O_TRUNC, 0o600), "w") as fh:
            fh.write("")
        print("new file", oct(stat.S_IMODE(os.lstat(f).st_mode)))
        os.chmod(f, 0o644)
        with os.fdopen(os.open(f, os.O_RDWR | os.O_CREAT | os.O_TRUNC, 0o600), "w") as fh:
            fh.write("")
        print("existing file", oct(stat.S_IMODE(os.lstat(f).st_mode)))
finally:
    os.umask(old_umask)
PY

sed -n '1,115p' pkg/catalog/loader/ai_loader.go
sed -n '120,180p' pkg/external/customtemplates/gitlab.go
sed -n '80,110p' pkg/external/customtemplates/s3.go
sed -n '95,120p' pkg/input/formats/openapi/downloader.go
sed -n '130,150p' pkg/input/formats/swagger/downloader.go

echo "Static analysis:"
go vet ./pkg/catalog/loader/ai_loader.go ./pkg/external/customtemplates/azure_blob.go ./pkg/external/customtemplates/gitlab.go ./pkg/external/customtemplates/s3.go ./pkg/input/formats/formats.go ./pkg/input/formats/openapi/downloader.go ./pkg/input/formats/swagger/downloader.go ./pkg/templates/template_sign.go

Repository: projectdiscovery/nuclei

Length of output: 462


🏁 Script executed:

#!/usr/bin/env bash
set -euo pipefail

echo "Go version:"
go version

python3 - <<'PY'
import os, stat, tempfile

print("MkdirAll existing directory modes")
for before_mode in (0o755, 0o777):
    with tempfile.TemporaryDirectory() as tmp:
        d = os.path.join(tmp, "existing")
        os.mkdir(d, before_mode)
        try:
            os.makedirs(d, 0o750)
        except FileExistsError:
            pass
        after_mode = stat.S_IMODE(os.lstat(d).st_mode)
        print("before", oct(before_mode), "after MkdirAll 0o750", oct(after_mode))

print("WriteFile existing file mode")
for before_mode in (0o644, 0o755):
    with tempfile.TemporaryDirectory() as tmp:
        f = os.path.join(tmp, "existing")
        open(f, "wb").close()
        os.chmod(f, before_mode)
        with open(f, "wb") as fh:
            fh.write(b"")
        after_mode = stat.S_IMODE(os.lstat(f).st_mode)
        print("before", oct(before_mode), "after WriteFile", oct(after_mode))

print("OpenFile O_CREATE O_TRUNC file modes with umask 0000")
old_umask = os.umask(0)
try:
    with tempfile.TemporaryDirectory() as tmp:
        f = os.path.join(tmp, "create")
        with os.fdopen(os.open(f, os.O_RDWR | os.O_CREAT | os.O_TRUNC, 0o600), "w") as fh:
            fh.write("")
        print("new file", oct(stat.S_IMODE(os.lstat(f).st_mode)))
        os.chmod(f, 0o644)
        with os.fdopen(os.open(f, os.O_RDWR | os.O_CREAT | os.O_TRUNC, 0o600), "w") as fh:
            fh.write("")
        print("existing file", oct(stat.S_IMODE(os.lstat(f).st_mode)))
finally:
    os.umask(old_umask)
PY

sed -n '1,115p' pkg/catalog/loader/ai_loader.go
sed -n '120,180p' pkg/external/customtemplates/gitlab.go
sed -n '80,110p' pkg/external/customtemplates/s3.go
sed -n '95,120p' pkg/input/formats/openapi/downloader.go
sed -n '130,150p' pkg/input/formats/swagger/downloader.go

echo "Static analysis:"
go vet ./pkg/catalog/loader/ai_loader.go ./pkg/external/customtemplates/azure_blob.go ./pkg/external/customtemplates/gitlab.go ./pkg/external/customtemplates/s3.go ./pkg/input/formats/formats.go ./pkg/input/formats/openapi/downloader.go ./pkg/input/formats/swagger/downloader.go ./pkg/templates/template_sign.go

Repository: projectdiscovery/nuclei

Length of output: 8840


🏁 Script executed:

#!/usr/bin/env bash
set -euo pipefail

echo "Inspect remaining files:"
sed -n '1,90p' pkg/input/formats/formats.go
sed -n '1,55p' pkg/input/formats/openapi/downloader.go
sed -n '115,150p' pkg/input/formats/swagger/downloader.go
sed -n '70,90p' pkg/external/customtemplates/azure_blob.go
sed -n '68,78p' pkg/external/customtemplates/gitlab.go
sed -n '110,120p' pkg/external/customtemplates/gitlab.go
sed -n '76,88p' pkg/templates/template_sign.go

echo "Run go vet per affected package:"
for pkg in pkg/catalog/loader pkg/external/customtemplates pkg/input/formats pkg/input/formats/openapi pkg/input/formats/swagger pkg/templates; do
  echo "--- $pkg ---"
  go vet "./$pkg/..."
done

echo "Find all os.Create/O_CREATE OpenFile in affected paths:"
rg -n 'os\.(Create|OpenFile|WriteFile|MkdirAll)\(' \
  pkg/catalog/loader \
  pkg/external/customtemplates \
  pkg/input/formats \
  pkg/templates

Repository: projectdiscovery/nuclei

Length of output: 13588


Enforce file permissions on reruns and default file creation.

MkdirAll and O_CREATE permission hints only affect newly created inodes. Existing directories remain at their current modes, existing files keep their previous modes, and os.Create uses 0666 before umask.

  • pkg/input/formats/formats.go#L103-L104: apply 0600 to an existing variable dump before truncating.
  • pkg/catalog/loader/ai_loader.go#L49-L54: check/fix the existing pdcp directory before writing the template.
  • pkg/external/customtemplates/azure_blob.go#L149-L156: check/fix existing parent directories before writing.
  • pkg/external/customtemplates/gitlab.go#L75-L173: check/fix existing directories before writing.
  • pkg/external/customtemplates/s3.go#L94-L101: keep managed directories restricted and create the file at 0600 instead of using os.Create.
  • pkg/input/formats/openapi/downloader.go#L107-L116: check/fix existing OpenAPI directory and create the spec at 0600 instead of using os.Create.
  • pkg/input/formats/swagger/downloader.go#L120-L145: check/fix existing Swagger directory and create the spec at 0600 instead of using os.Create.
  • pkg/templates/template_sign.go#L83-L85: apply 0600 to an existing template before writing.

Add rerun tests with pre-existing 0755 directories and 0644 files.

📍 Affects 8 files
  • pkg/input/formats/formats.go#L103-L104 (this comment)
  • pkg/catalog/loader/ai_loader.go#L49-L54
  • pkg/external/customtemplates/azure_blob.go#L149-L156
  • pkg/external/customtemplates/gitlab.go#L75-L75
  • pkg/external/customtemplates/gitlab.go#L115-L115
  • pkg/external/customtemplates/gitlab.go#L165-L173
  • pkg/external/customtemplates/s3.go#L94-L96
  • pkg/input/formats/openapi/downloader.go#L107-L107
  • pkg/input/formats/swagger/downloader.go#L120-L120
  • pkg/templates/template_sign.go#L83-L85
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@pkg/input/formats/formats.go` around lines 103 - 104, Enforce restrictive
permissions on reruns: in pkg/input/formats/formats.go:103-104, update
WriteOpenAPIVarDumpFile to apply 0600 to the existing dump before truncating.
Check and fix existing directories before writing in
pkg/catalog/loader/ai_loader.go:49-54,
pkg/external/customtemplates/azure_blob.go:149-156,
pkg/external/customtemplates/gitlab.go:75, 115, and 165-173,
pkg/input/formats/openapi/downloader.go:107, and
pkg/input/formats/swagger/downloader.go:120; in
pkg/external/customtemplates/s3.go:94-96 keep managed directories restricted and
create files with 0600, and do the same for OpenAPI and Swagger specs instead of
os.Create. Apply 0600 to existing templates in
pkg/templates/template_sign.go:83-85, and add rerun tests covering pre-existing
0755 directories and 0644 files.

return err
}
if err := os.WriteFile(config.DefaultConfig.GetIgnoreFilePath(), bin, 0644); err != nil {
if err := os.WriteFile(config.DefaultConfig.GetIgnoreFilePath(), bin, 0600); err != nil {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail
go doc os.OpenFile os.WriteFile
rg -n -C2 'os\.(OpenFile|WriteFile)|\.Chmod' \
  pkg/installer/versioncheck.go \
  pkg/output/file_output_writer.go \
  pkg/output/output.go \
  pkg/reporting/exporters/jsonexporter/jsonexporter.go \
  pkg/reporting/exporters/jsonl/jsonl.go \
  pkg/reporting/exporters/sarif/sarif.go

Repository: projectdiscovery/nuclei

Length of output: 195


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '## Go standard library references (%s)\n' "$(go version 2>/dev/null || true)"
go doc os.OpenFile | sed -n '1,80p' || true
printf '\n## Go standard library references for WriteFile\n'
go doc os.WriteFile | sed -n '1,80p' || true

printf '\n## Affected files and relevant symbols\n'
rg -n -C3 'os\.(OpenFile|WriteFile)|\.Chmod|O_APPEND|O_TRUNC|O_CREATE|0600|0644' \
  pkg/installer/versioncheck.go \
  pkg/output/file_output_writer.go \
  pkg/output/output.go \
  pkg/reporting/exporters/jsonexporter/jsonexporter.go \
  pkg/reporting/exporters/jsonl/jsonl.go \
  pkg/reporting/exporters/sarif/sarif.go || true

Repository: projectdiscovery/nuclei

Length of output: 4509


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '## Behavioral probe for os.OpenFile with existing file\n'
tmp="$(mktemp -d)"
trap 'rm -rf "$tmp"' EXIT
file="$tmp/existing.txt"
printf 'world\n' > "$file"
chmod 0644 "$file"
stat_before="$(stat -c '%a' "$file")"
echo "before: $stat_before"
go run - <<'GO' >/tmp/osprobe.out 2>&1 || true
package main

import (
    "fmt"
    "os"
    "path/filepath"
)

func main() {
    tmp := os.Args[1]
    file := filepath.Join(tmp, "existing.txt")

    f, err := os.OpenFile(file, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0600)
    if err != nil {
        fmt.Fprintln(os.Stderr, err)
        os.Exit(1)
    }
    defer f.Close()

    fmt.Println(f.Name(), os.PathSep)
}
GO
cat /tmp/osprobe.out
stat_after="$(stat -c '%a' "$file")"
echo "after append with O_APPEND|O_CREATE: $stat_after"

printf '\n## Behavioral probe for a read-truncate-open path\n'
file="$tmp/existing2.txt"
printf 'world\n' > "$file"
chmod 0644 "$file"
echo "before: $(stat -c '%a' "$file")"
go run - <<'GO' >/tmp/osprobe2.out 2>&1 || true
package main

import "os"

func main() {
    f, err := os.OpenFile("nonexistent_probe.txt", os.O_RDWR|os.O_TRUNC|os.O_CREATE, 0640)
    if err != nil {
        panic(err)
    }
    f.Close()
}
GO
cat /tmp/osprobe2.out
stat_after="$(stat -c '%a' "$file")"
echo "after separate read+write/truncate: $stat_after"

Repository: projectdiscovery/nuclei

Length of output: 259


Enforce owner-only permissions on existing files.

os.OpenFile and os.WriteFile only set the mode when creating a new file; existing files keep their current permissions. Existing 0644 files can stay readable by group and other users when the application appends, truncates, or overwrites them.

  • pkg/installer/versioncheck.go#L79: enforce 0600 on the existing ignore file.
  • pkg/output/file_output_writer.go#L19-L21: apply Chmod(0600) in both resume and non-resume paths.
  • pkg/output/output.go#L650: apply Chmod(0600) before appending debug data.
  • pkg/reporting/exporters/jsonexporter/jsonexporter.go#L65: write through an opened descriptor and enforce 0600.
  • pkg/reporting/exporters/jsonl/jsonl.go#L73: apply Chmod(0600) before writing rows.
  • pkg/reporting/exporters/sarif/sarif.go#L191: write through an opened descriptor and enforce 0600.
📍 Affects 6 files
  • pkg/installer/versioncheck.go#L79-L79 (this comment)
  • pkg/output/file_output_writer.go#L19-L21
  • pkg/output/output.go#L650-L650
  • pkg/reporting/exporters/jsonexporter/jsonexporter.go#L65-L65
  • pkg/reporting/exporters/jsonl/jsonl.go#L73-L73
  • pkg/reporting/exporters/sarif/sarif.go#L191-L191
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@pkg/installer/versioncheck.go` at line 79, Enforce owner-only permissions on
existing output files: in pkg/installer/versioncheck.go:79 apply Chmod(0600) to
the ignore file after opening or writing it; in
pkg/output/file_output_writer.go:19-21 apply Chmod(0600) in both resume and
non-resume paths; in pkg/output/output.go:650 apply Chmod(0600) before appending
debug data; in pkg/reporting/exporters/jsonexporter/jsonexporter.go:65 and
pkg/reporting/exporters/sarif/sarif.go:191 write through opened descriptors and
enforce 0600; and in pkg/reporting/exporters/jsonl/jsonl.go:73 apply Chmod(0600)
before writing rows.

claude and others added 3 commits August 3, 2026 06:08
ParseRawRequest read the request line and headers positionally: the
second line was always treated as the Host line regardless of content,
and header values were read by skipping exactly one byte past the
colon. This meant:
- a valueless header (e.g. "X-Empty:") panicked with a slice-bounds
  error, taking down the whole scan
- a Host header anywhere but the second line was silently dropped from
  the header map, so header-based preconditions never matched
- "Host:target" (no space after colon) parsed the host as "arget"
- an absolute-form request target, as used by proxy captures
  (GET http://target/p HTTP/1.1), got appended to the Host rather than
  replacing it

Every request-shaped input mode (burp, jsonl, yaml, openapi) goes
through this parser. Parse headers uniformly first, look up Host by
name, trim header values instead of assuming one space, and treat an
absolute-form request target as replacing the authority outright -
matching how pkg/protocols/http/raw.readRawRequest already handles
raw template requests.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QSCzGpK3LLQDKXtHCew5CG
Fix ParseRawRequest header/host parsing panics and data loss

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@pkg/input/types/http.go`:
- Around line 257-261: Update the header parsing logic in the HTTP request
parser to recognize the Host header case-insensitively using strings.EqualFold
(or canonicalize names before storage), ensuring lower-case host values populate
rr.URL.Host for relative targets. Preserve existing header storage behavior and
add a regression test covering a lower-case host header.
- Line 264: Update the absolute-target detection in the HTTP request-target
parsing logic to perform case-insensitive HTTP/HTTPS prefix checks by
normalizing only the value used for detection, while continuing to parse the
original requestTarget unchanged. Add a regression test covering an uppercase
scheme such as HTTP:// and verify it uses absolute-form authority handling.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 4cb11b5c-a6f5-4a0a-be5c-d382fc6b2ed5

📥 Commits

Reviewing files that changed from the base of the PR and between 675fcc4 and 7f2254a.

📒 Files selected for processing (2)
  • pkg/input/types/http.go
  • pkg/input/types/http_test.go

Comment thread pkg/input/types/http.go
Comment on lines +257 to +261
headerParts := strings.SplitN(line, ":", 2)
if len(headerParts) != 2 {
return nil, fmt.Errorf("invalid header line: %s", line)
}
rr.Request.Headers.Set(parts[0], parts[1][1:])
rr.Request.Headers.Set(strings.TrimSpace(headerParts[0]), strings.TrimSpace(headerParts[1]))

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Resolve the Host header without case sensitivity.

HTTP header names are case-insensitive. The parser preserves the input key casing, but later looks up only "Host". A valid host: target header leaves rr.URL.Host empty for a relative request target.

Capture the Host value with strings.EqualFold while parsing, or canonicalize header names before storage. Add a regression test for a lower-case host header.

Proposed fix
 rr.Request.Headers = mapsutil.NewOrderedMap[string, string]()
+var host string
 for {
   // ...
-  rr.Request.Headers.Set(strings.TrimSpace(headerParts[0]), strings.TrimSpace(headerParts[1]))
+  headerName := strings.TrimSpace(headerParts[0])
+  headerValue := strings.TrimSpace(headerParts[1])
+  rr.Request.Headers.Set(headerName, headerValue)
+  if strings.EqualFold(headerName, "Host") {
+    host = headerValue
+  }
 }
 // ...
-  if host, ok := rr.Request.Headers.Get("Host"); ok {
-    rr.URL.Host = strings.TrimSpace(host)
+  if host != "" {
+    rr.URL.Host = host
   }

Also applies to: 279-280

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@pkg/input/types/http.go` around lines 257 - 261, Update the header parsing
logic in the HTTP request parser to recognize the Host header case-insensitively
using strings.EqualFold (or canonicalize names before storage), ensuring
lower-case host values populate rr.URL.Host for relative targets. Preserve
existing header storage behavior and add a regression test covering a lower-case
host header.

Comment thread pkg/input/types/http.go
rr.Request.Headers.Set(strings.TrimSpace(headerParts[0]), strings.TrimSpace(headerParts[1]))
}

if strings.HasPrefix(requestTarget, "http://") || strings.HasPrefix(requestTarget, "https://") {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Detect absolute HTTP targets without case sensitivity.

URI schemes are case-insensitive. An absolute request target beginning with HTTP:// or HTTPS:// does not enter this branch. It therefore misses the required absolute-form authority handling.

Normalize only the value used for prefix detection. Parse the original request target. Add a regression test with an upper-case scheme.

Proposed fix
-if strings.HasPrefix(requestTarget, "http://") || strings.HasPrefix(requestTarget, "https://") {
+normalizedRequestTarget := strings.ToLower(requestTarget)
+if strings.HasPrefix(normalizedRequestTarget, "http://") || strings.HasPrefix(normalizedRequestTarget, "https://") {
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if strings.HasPrefix(requestTarget, "http://") || strings.HasPrefix(requestTarget, "https://") {
normalizedRequestTarget := strings.ToLower(requestTarget)
if strings.HasPrefix(normalizedRequestTarget, "http://") || strings.HasPrefix(normalizedRequestTarget, "https://") {
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@pkg/input/types/http.go` at line 264, Update the absolute-target detection in
the HTTP request-target parsing logic to perform case-insensitive HTTP/HTTPS
prefix checks by normalizing only the value used for detection, while continuing
to parse the original requestTarget unchanged. Add a regression test covering an
uppercase scheme such as HTTP:// and verify it uses absolute-form authority
handling.

claude and others added 3 commits August 3, 2026 06:21
The DAST fuzzing/stats server (-dast-server) constructed its http.Server
with no timeouts, leaving it open to a Slowloris-style slow-header DoS.
Only ReadHeaderTimeout is set (not ReadTimeout/WriteTimeout) since /fuzz
requests can legitimately take a while to process. Same class of fix
already applied to internal/httpapi/apiendpoint.go.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QSCzGpK3LLQDKXtHCew5CG
Add ReadHeaderTimeout to DASTServer's http.Server
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants