Skip to content

feat(reporting): add PDF exporter for scan results#7074

Open
yuliuyi717-ux wants to merge 4 commits intoprojectdiscovery:devfrom
yuliuyi717-ux:codex/pdf-exporter-2063
Open

feat(reporting): add PDF exporter for scan results#7074
yuliuyi717-ux wants to merge 4 commits intoprojectdiscovery:devfrom
yuliuyi717-ux:codex/pdf-exporter-2063

Conversation

@yuliuyi717-ux
Copy link

@yuliuyi717-ux yuliuyi717-ux commented Feb 27, 2026

Proposed changes

  • add a new pdf exporter under pkg/reporting/exporters/pdf that implements the existing reporting Exporter interface
  • include report sections for summary + severity counts + findings overview + per-finding details
  • include optional raw request/response blocks with bounded truncation and omit-raw support
  • make exporter safe for concurrent Export calls by guarding shared state and snapshotting before render
  • wire PDF exporter into reporting configuration (yaml: pdf) and exporter registration in pkg/reporting/reporting.go
  • add focused unit tests for constructor behavior, nil event handling, omit-raw behavior, truncation, and concurrency

Proof

Executed locally:

go test ./pkg/reporting/exporters/pdf
go test -race ./pkg/reporting/exporters/pdf
go test ./pkg/reporting/...

Checklist

  • Pull request is created against the dev branch
  • All checks passed (lint, unit/integration/regression tests etc.) with my changes
  • I have added tests that prove my fix is effective or that my feature works
  • I have added necessary documentation (if appropriate)

/claim #2063

Summary by CodeRabbit

  • New Features

    • PDF export for scan findings with configurable output path, header, severity summary, findings overview, and detailed per-finding sections.
    • Option to omit raw request/response blocks and safe truncation of large raw content.
    • Thread-safe result collection and safe handling of output directories.
  • Tests

    • Comprehensive tests for PDF output, omission/truncation behavior, concurrency, and file handling.
  • Chores

    • Added PDF generation dependency.

@neo-by-projectdiscovery-dev
Copy link

neo-by-projectdiscovery-dev bot commented Feb 27, 2026

Neo - PR Security Review

Medium: 2

Highlights

  • Adds deep cloning mechanism for ResultEvent objects to prevent concurrent mutation bugs
  • Implements cloneResultEvent() with helpers for Info, metadata maps, issue trackers, and file index maps
  • Updates Export() method to snapshot events via cloning before storing them
  • Adds TestExportDeepCopiesMutableFields to verify clone isolation
Medium (2)
  • Absolute path writes still allowed in PDF exporterpkg/reporting/exporters/pdf/pdf.go:58
    The path validation added in lines 54-56 only checks for parent directory traversal sequences ('..') but does not prevent absolute paths. An attacker who controls the reporting configuration can still specify absolute paths like '/etc/nuclei-report.pdf' or 'C:\Windows\System32\report.pdf' to write PDF files to arbitrary filesystem locations.
  • Absolute path writes still allowed in PDF exporterpkg/reporting/exporters/pdf/pdf.go:61
    The path validation added in lines 54-56 only checks for parent directory traversal sequences ('..') but does not prevent absolute paths. An attacker who controls the reporting configuration can still specify absolute paths like '/etc/nuclei-report.pdf' or 'C:\Windows\System32\report.pdf' to write PDF files to arbitrary filesystem locations.
Attack Examples

Absolute path writes still allowed in PDF exporter (pkg/reporting/exporters/pdf/pdf.go:58):

reporting:
  pdf:
    file: "/etc/cron.d/malicious-job"  # Absolute path to system directory
    # OR on Windows:
    file: "C:\\Windows\\System32\\report.pdf"

Absolute path writes still allowed in PDF exporter (pkg/reporting/exporters/pdf/pdf.go:61):

reporting:
  pdf:
    file: "/etc/cron.d/malicious-job"  # Absolute path to system directory
    # OR on Windows:
    file: "C:\\Windows\\System32\\report.pdf"
Suggested Fixes

Absolute path writes still allowed in PDF exporter (pkg/reporting/exporters/pdf/pdf.go:58):

Add absolute path validation after line 53: Check if the path is absolute using filepath.IsAbs(options.File) and reject it with an error, OR establish a base directory and validate that the resolved absolute path stays within that directory using filepath.Abs() and strings.HasPrefix().

Absolute path writes still allowed in PDF exporter (pkg/reporting/exporters/pdf/pdf.go:61):

Add absolute path validation after line 53: Check if the path is absolute using filepath.IsAbs(options.File) and reject it with an error, OR establish a base directory and validate that the resolved absolute path stays within that directory using filepath.Abs() and strings.HasPrefix().
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

Verify each finding against the current code and only fix it if needed.

In @pkg/reporting/exporters/pdf/pdf.go after line 53 (after the empty file
check), add absolute path validation by inserting: if filepath.IsAbs(options.File) { return nil, fmt.Errorf("invalid PDF report path: absolute paths are not allowed") } to prevent arbitrary file writes to system
directories via absolute paths in the configuration.


Verify each finding against the current code and only fix it if needed.

In @pkg/reporting/exporters/pdf/pdf.go after line 53 (after the empty file
check), add absolute path validation by inserting: if filepath.IsAbs(options.File) { return nil, fmt.Errorf("invalid PDF report path: absolute paths are not allowed") } to prevent arbitrary file writes to system
directories via absolute paths in the configuration.

Comment @neo help for available commands. · Open in Neo

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Feb 27, 2026

Walkthrough

Adds a new PDF exporter that buffers ResultEvent entries and generates a styled PDF on Close (configurable output path and omission of raw blocks). Tests and reporting integration were added; go.mod gains an indirect gofpdf dependency.

Changes

Cohort / File(s) Summary
PDF Exporter Implementation
pkg/reporting/exporters/pdf/pdf.go
New exporter: Options (File, OmitRaw), Exporter with mutex-protected results buffer, New(), Export(), Close(), PDF generation and helpers (severity colors, truncation, formatting) using gofpdf.
PDF Exporter Tests
pkg/reporting/exporters/pdf/pdf_test.go
New tests covering default behavior, directory creation, nil-event handling, Close() with/without results, OmitRaw behavior and truncation, concurrency/thread-safety, and helper builders.
Reporting Integration
pkg/reporting/options.go, pkg/reporting/reporting.go
Imported pdf exporter, added PDFExporter *pdf.Options \yaml:"pdf"`to Options, wired exporter creation/registration inNew()`, and initialized default PDFExporter in config creation.
Dependencies
go.mod
Added indirect dependency: github.com/phpdave11/gofpdf v1.4.3.

Sequence Diagram

sequenceDiagram
    participant Config as Configuration
    participant Reporting as Reporting System
    participant Exporter as PDF Exporter
    participant Buffer as Results Buffer
    participant PDF as gofpdf
    participant FS as File System

    Config->>Reporting: Initialize with PDFExporter options
    Reporting->>Exporter: New(options)
    Exporter->>FS: Ensure output directory / validate path
    Reporting->>Exporter: Export(result event)
    Exporter->>Buffer: Lock & append deep-copied result
    Reporting->>Exporter: Export(result event)
    Exporter->>Buffer: Lock & append deep-copied result
    Reporting->>Exporter: Close()
    Exporter->>Buffer: Lock, snapshot buffered results
    Exporter->>PDF: Build report (header, summary, findings, details)
    PDF->>PDF: Render pages (colors, truncation, code blocks)
    PDF->>FS: Write PDF file to disk
    FS-->>Exporter: File write success
    Exporter-->>Reporting: Close returns
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~30 minutes

Poem

🐰 I hopped through findings crisp and bright,
I buffered every match by day and night.
With colors, trims, and headers all aligned,
I stitched a PDF report for humankind.
Tail twitch, I stamped the page — export defined.

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 10.71% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'feat(reporting): add PDF exporter for scan results' directly and clearly summarizes the main change in the pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

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 and usage tips.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (1)
pkg/reporting/exporters/pdf/pdf_test.go (1)

114-119: Don’t ignore Export errors in the concurrency test.

This can hide regressions and let the test pass when concurrent exports fail.

Proposed test hardening
 	var wg sync.WaitGroup
+	errCh := make(chan error, 30)
 	for i := 0; i < 30; i++ {
 		wg.Add(1)
 		go func(index int) {
 			defer wg.Done()
 			event := buildEvent("concurrent.example.com", severity.Low)
 			event.TemplateID = "tmpl-concurrent-" + time.Unix(int64(index), 0).UTC().Format("150405")
-			_ = exporter.Export(event)
+			errCh <- exporter.Export(event)
 		}(i)
 	}
 	wg.Wait()
+	close(errCh)
+	for err := range errCh {
+		require.NoError(t, err)
+	}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pkg/reporting/exporters/pdf/pdf_test.go` around lines 114 - 119, The
goroutine in the concurrency test is ignoring the result of exporter.Export
which can hide failures; modify the test to capture and assert the Export error
instead of discarding it: create a buffered errs channel (or slice protected by
mutex), have the goroutine send the error returned by exporter.Export(event) to
that channel (reference the anonymous goroutine, exporter.Export, buildEvent and
event.TemplateID), wait for wg, close the channel and then iterate over
collected errors and fail the test on any non-nil error (e.g. t.Fatalf or
t.Errorf/require.NoError for each) so concurrent export failures surface.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@pkg/reporting/exporters/pdf/pdf.go`:
- Line 96: Remove the explicit disabling of PDF compression by deleting the call
to pdf.SetCompression(false) (the pdf.SetCompression call in pdf.go) so
compression remains enabled by default; locate where pdf.SetCompression(false)
is invoked (in the PDF exporter initialization) and remove that line to avoid
inflated report sizes in production.

---

Nitpick comments:
In `@pkg/reporting/exporters/pdf/pdf_test.go`:
- Around line 114-119: The goroutine in the concurrency test is ignoring the
result of exporter.Export which can hide failures; modify the test to capture
and assert the Export error instead of discarding it: create a buffered errs
channel (or slice protected by mutex), have the goroutine send the error
returned by exporter.Export(event) to that channel (reference the anonymous
goroutine, exporter.Export, buildEvent and event.TemplateID), wait for wg, close
the channel and then iterate over collected errors and fail the test on any
non-nil error (e.g. t.Fatalf or t.Errorf/require.NoError for each) so concurrent
export failures surface.

ℹ️ Review info

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 73507cd and 0a6f0e1.

⛔ Files ignored due to path filters (1)
  • go.sum is excluded by !**/*.sum
📒 Files selected for processing (5)
  • go.mod
  • pkg/reporting/exporters/pdf/pdf.go
  • pkg/reporting/exporters/pdf/pdf_test.go
  • pkg/reporting/options.go
  • pkg/reporting/reporting.go

@yuliuyi717-ux
Copy link
Author

Formatting-safe follow-up:

Addressed the open review findings in commit c2544f5:

  • removed forced pdf.SetCompression(false), so exporter uses default compression
  • added parent traversal guard for output path (".." segment) plus filepath.Clean normalization
  • hardened concurrent export test to assert Export errors instead of discarding them
  • added traversal rejection regression test

Validation run:

  • go test ./pkg/reporting/exporters/pdf -count=1

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

♻️ Duplicate comments (1)
pkg/reporting/exporters/pdf/pdf.go (1)

54-58: ⚠️ Potential issue | 🟠 Major

Absolute output paths still bypass the path-hardening guard.

Line 54 blocks .. segments, but Line 57/Line 114 still allow absolute paths (for example /tmp/report.pdf), which can write outside the project working tree.

Suggested fix
 func New(options *Options) (*Exporter, error) {
 	if options == nil {
 		options = &Options{}
 	}
 	if options.File == "" {
 		options.File = defaultFileName
 	}
-	if hasParentPathSegment(options.File) {
+	cleaned := filepath.Clean(options.File)
+	if filepath.IsAbs(cleaned) {
+		return nil, fmt.Errorf("invalid PDF report path: absolute paths are not allowed")
+	}
+	if hasParentPathSegment(cleaned) {
 		return nil, fmt.Errorf("invalid PDF report path: parent directory traversal is not allowed")
 	}
-	options.File = filepath.Clean(options.File)
+	options.File = cleaned
 	if dir := filepath.Dir(options.File); dir != "" && dir != "." {
 		if err := os.MkdirAll(dir, 0755); err != nil {
 			return nil, fmt.Errorf("could not create directory for PDF report: %w", err)
 		}
 	}

Also applies to: 114-114

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pkg/reporting/exporters/pdf/pdf.go` around lines 54 - 58, The path-hardening
misses absolute paths: update the validation around options.File (the branch
that currently calls hasParentPathSegment(options.File) and then
filepath.Clean/Dir) to also reject absolute paths by checking
filepath.IsAbs(options.File) (and return an error like "absolute paths not
allowed") before cleaning; apply the same absolute-path check to the other
occurrence that validates options.File so both code paths (the initial validator
and the later validator near the second use) consistently deny absolute output
paths.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@pkg/reporting/exporters/pdf/pdf.go`:
- Around line 54-58: The path-hardening misses absolute paths: update the
validation around options.File (the branch that currently calls
hasParentPathSegment(options.File) and then filepath.Clean/Dir) to also reject
absolute paths by checking filepath.IsAbs(options.File) (and return an error
like "absolute paths not allowed") before cleaning; apply the same absolute-path
check to the other occurrence that validates options.File so both code paths
(the initial validator and the later validator near the second use) consistently
deny absolute output paths.

ℹ️ Review info

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 0a6f0e1 and c2544f5.

📒 Files selected for processing (2)
  • pkg/reporting/exporters/pdf/pdf.go
  • pkg/reporting/exporters/pdf/pdf_test.go

@yuliuyi717-ux
Copy link
Author

Follow-up update (absolute-path hardening):

Addressed in commit 4745b84:

  • New now rejects absolute output paths via filepath.IsAbs.
  • Existing parent traversal check (".." segments) remains in place.
  • Added TestNewRejectsAbsolutePath.
  • Updated PDF exporter tests to use temporary working directories with relative paths, keeping the suite aligned with the new path policy.

Validation:

  • go test ./pkg/reporting/exporters/pdf -count=1

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (1)
pkg/reporting/exporters/pdf/pdf.go (1)

78-81: Consider implications of shallow copy for event storage.

The struct assignment eventCopy := *event creates a shallow copy. If ResultEvent contains pointer fields, slices, or maps (e.g., ExtractedResults, Info.Reference), those remain shared with the original. If the caller modifies nested fields after calling Export(), the stored data could be affected.

This is generally acceptable if callers follow the convention of not mutating events post-emission, but worth documenting or verifying against the project's usage patterns.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pkg/reporting/exporters/pdf/pdf.go` around lines 78 - 81, The code uses a
shallow copy (eventCopy := *event) before appending to e.results which leaves
nested pointers/slices/maps shared; change the Export path that appends to
e.results to perform a deep copy of ResultEvent (clone all nested mutable fields
such as ExtractedResults, Info.Reference, any slices/maps or pointer
sub-structs) before appending under e.mu.Lock, so stored events are independent
of future mutations; locate the append site where e.results is modified and
implement a deep-copy/clone helper for ResultEvent and use it when appending.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@pkg/reporting/exporters/pdf/pdf.go`:
- Around line 155-165: The loop that tallies severities iterates over results
but only checks result for nil before dereferencing result.Info; update the nil
checks to guard against a nil Info (and nested SeverityHolder) before accessing
result.Info.SeverityHolder.Severity in the counting loop (the same pattern
should be applied in writeFindingsTable and writeDetailedFindings): ensure each
loop first checks result != nil && result.Info != nil &&
result.Info.SeverityHolder != nil (or otherwise defensively handle missing
fields) before computing severity and updating the counts map (fall back to
"unknown" when any of those are nil).

---

Nitpick comments:
In `@pkg/reporting/exporters/pdf/pdf.go`:
- Around line 78-81: The code uses a shallow copy (eventCopy := *event) before
appending to e.results which leaves nested pointers/slices/maps shared; change
the Export path that appends to e.results to perform a deep copy of ResultEvent
(clone all nested mutable fields such as ExtractedResults, Info.Reference, any
slices/maps or pointer sub-structs) before appending under e.mu.Lock, so stored
events are independent of future mutations; locate the append site where
e.results is modified and implement a deep-copy/clone helper for ResultEvent and
use it when appending.

ℹ️ Review info

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between c2544f5 and 4745b84.

📒 Files selected for processing (2)
  • pkg/reporting/exporters/pdf/pdf.go
  • pkg/reporting/exporters/pdf/pdf_test.go
🚧 Files skipped from review as they are similar to previous changes (1)
  • pkg/reporting/exporters/pdf/pdf_test.go

@yuliuyi717-ux
Copy link
Author

Follow-up update: pushed one more hardening commit (659e110) to this branch.

What changed:

  • PDF exporter now deep-copies ResultEvent before buffering it in Export, instead of using a shallow struct copy.
  • This avoids later caller-side mutations leaking into buffered report data (slices/maps/pointer-backed fields).
  • Added regression test TestExportDeepCopiesMutableFields that mutates the original event after Export and verifies buffered data remains stable.

Validation:

  • go test ./pkg/reporting/exporters/pdf -count=1
  • go test ./pkg/reporting/... -count=1

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

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 the current code and only fix it if needed.

Inline comments:
In `@pkg/reporting/exporters/pdf/pdf_test.go`:
- Around line 188-194: Test constructs a path that isn't guaranteed to be
OS-absolute on Windows; change the absolutePath setup in
pkg/reporting/exporters/pdf/pdf_test.go to produce a true absolute path by
wrapping the joined path with filepath.Abs (e.g. set absolutePath =
filepath.Abs(filepath.Join(string(os.PathSeparator), "tmp", "outside",
"report.pdf")) and handle the returned error before calling New). Update the
variable used in the New(&Options{File: absolutePath}) call so the test uses the
OS-absolute path.

In `@pkg/reporting/exporters/pdf/pdf.go`:
- Around line 48-70: The New function currently validates the caller-supplied
*Options and then stores the same pointer into Exporter, allowing TOCTOU
mutation; instead, make a defensive copy of Options at the start of New (e.g.,
allocate a new Options value and copy the fields from the incoming *options or
use a shallow copy and then normalize/clean fields on the copy), run all path
validation and normalization against that copy (including filepath.Clean, IsAbs,
hasParentPathSegment, and directory creation), and then store the copied Options
on the returned Exporter (Exporter.options = copiedOptions) so the original
caller pointer cannot modify validated fields later (ensure behavior of results
initialization remains unchanged).

ℹ️ Review info

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 4745b84 and 659e110.

📒 Files selected for processing (2)
  • pkg/reporting/exporters/pdf/pdf.go
  • pkg/reporting/exporters/pdf/pdf_test.go

Comment on lines +188 to +194
absolutePath := filepath.Join(string(os.PathSeparator), "tmp", "outside", "report.pdf")

_, err := New(&Options{File: absolutePath})

require.Error(t, err)
require.Contains(t, err.Error(), "absolute path")
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

# First, let's look at the actual test file to see the exact code
cat -n pkg/reporting/exporters/pdf/pdf_test.go | sed -n '180,210p'

Repository: projectdiscovery/nuclei

Length of output: 1018


🏁 Script executed:

# Also check the pdf.go file to understand what validation it does
cat -n pkg/reporting/exporters/pdf/pdf.go | head -100

Repository: projectdiscovery/nuclei

Length of output: 3287


🌐 Web query:

filepath.IsAbs behavior Windows "\tmp\report.pdf" vs Unix cross-platform Go path validation

💡 Result:

What filepath.IsAbs does with "\tmp\report.pdf" on Windows vs Unix

  • On Unix-like systems: filepath.IsAbs("/tmp/report.pdf") == true because paths starting with / are absolute. (pkg.go.dev)
  • On Windows: filepath.IsAbs("\\tmp\\report.pdf") == false (and likewise "/tmp/report.pdf" == false) because an absolute Windows path normally includes a volume (drive letter like C:\... or a UNC share like \\host\share\...). A leading slash/backslash without a volume is treated as not absolute by Go’s Windows rules. (stackoverflow.com)

Windows examples that are absolute to filepath.IsAbs

Cross-platform path validation guidance in Go

  • Use path/filepath for OS-native filesystem paths; the path package is for forward-slash paths like URLs and will mis-handle Windows paths. (stackoverflow.com)
  • If you need to validate “absolute on either Windows or Unix” regardless of the current OS, you generally must implement an OS-agnostic check (e.g., accept either ^[A-Za-z]:[\\/]|^\\\\\\\\ for Windows, or ^/ for Unix), because filepath.IsAbs is explicitly OS-dependent. (pkg.go.dev)

Citations:


Use filepath.Abs() to construct a truly absolute path in the absolute-path rejection test.

The test constructs \tmp\... style paths, which filepath.IsAbs does not recognize as absolute on Windows (only paths with a drive letter like C:\... are absolute on Windows). This causes the test to fail on Windows, defeating its purpose.

Suggested fix
 func TestNewRejectsAbsolutePath(t *testing.T) {
-	absolutePath := filepath.Join(string(os.PathSeparator), "tmp", "outside", "report.pdf")
+	absolutePath, err := filepath.Abs(filepath.Join("tmp", "outside", "report.pdf"))
+	require.NoError(t, err)
 
-	_, err := New(&Options{File: absolutePath})
+	_, err = New(&Options{File: absolutePath})
 
 	require.Error(t, err)
 	require.Contains(t, err.Error(), "absolute path")
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pkg/reporting/exporters/pdf/pdf_test.go` around lines 188 - 194, Test
constructs a path that isn't guaranteed to be OS-absolute on Windows; change the
absolutePath setup in pkg/reporting/exporters/pdf/pdf_test.go to produce a true
absolute path by wrapping the joined path with filepath.Abs (e.g. set
absolutePath = filepath.Abs(filepath.Join(string(os.PathSeparator), "tmp",
"outside", "report.pdf")) and handle the returned error before calling New).
Update the variable used in the New(&Options{File: absolutePath}) call so the
test uses the OS-absolute path.

Comment on lines +48 to +70
func New(options *Options) (*Exporter, error) {
if options == nil {
options = &Options{}
}
if options.File == "" {
options.File = defaultFileName
}
if filepath.IsAbs(options.File) {
return nil, fmt.Errorf("invalid PDF report path: absolute path is not allowed")
}
if hasParentPathSegment(options.File) {
return nil, fmt.Errorf("invalid PDF report path: parent directory traversal is not allowed")
}
options.File = filepath.Clean(options.File)
if dir := filepath.Dir(options.File); dir != "" && dir != "." {
if err := os.MkdirAll(dir, 0755); err != nil {
return nil, fmt.Errorf("could not create directory for PDF report: %w", err)
}
}
return &Exporter{
options: options,
results: make([]*output.ResultEvent, 0),
}, nil
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Copy Options in New to prevent post-validation mutation (TOCTOU).

Line 68 stores the caller’s *Options directly. That allows options.File to be modified after validation and before Close, bypassing the hardened path checks and creating concurrency risk.

Suggested fix
 func New(options *Options) (*Exporter, error) {
-	if options == nil {
-		options = &Options{}
-	}
-	if options.File == "" {
-		options.File = defaultFileName
+	resolved := Options{}
+	if options != nil {
+		resolved = *options
+	}
+	if resolved.File == "" {
+		resolved.File = defaultFileName
 	}
-	if filepath.IsAbs(options.File) {
+	if filepath.IsAbs(resolved.File) {
 		return nil, fmt.Errorf("invalid PDF report path: absolute path is not allowed")
 	}
-	if hasParentPathSegment(options.File) {
+	if hasParentPathSegment(resolved.File) {
 		return nil, fmt.Errorf("invalid PDF report path: parent directory traversal is not allowed")
 	}
-	options.File = filepath.Clean(options.File)
-	if dir := filepath.Dir(options.File); dir != "" && dir != "." {
+	resolved.File = filepath.Clean(resolved.File)
+	if dir := filepath.Dir(resolved.File); dir != "" && dir != "." {
 		if err := os.MkdirAll(dir, 0755); err != nil {
 			return nil, fmt.Errorf("could not create directory for PDF report: %w", err)
 		}
 	}
 	return &Exporter{
-		options: options,
+		options: &resolved,
 		results: make([]*output.ResultEvent, 0),
 	}, nil
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pkg/reporting/exporters/pdf/pdf.go` around lines 48 - 70, The New function
currently validates the caller-supplied *Options and then stores the same
pointer into Exporter, allowing TOCTOU mutation; instead, make a defensive copy
of Options at the start of New (e.g., allocate a new Options value and copy the
fields from the incoming *options or use a shallow copy and then normalize/clean
fields on the copy), run all path validation and normalization against that copy
(including filepath.Clean, IsAbs, hasParentPathSegment, and directory creation),
and then store the copied Options on the returned Exporter (Exporter.options =
copiedOptions) so the original caller pointer cannot modify validated fields
later (ensure behavior of results initialization remains unchanged).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant