Skip to content

fix(authx): prevent race condition and enable retry on fetch failure#7110

Closed
hanzhcn wants to merge 12 commits intoprojectdiscovery:devfrom
hanzhcn:fix/auth-scan-race-6592
Closed

fix(authx): prevent race condition and enable retry on fetch failure#7110
hanzhcn wants to merge 12 commits intoprojectdiscovery:devfrom
hanzhcn:fix/auth-scan-race-6592

Conversation

@hanzhcn
Copy link

@hanzhcn hanzhcn commented Mar 4, 2026

Summary

This PR fixes Issue #6592 with three critical improvements:

1. Race Condition Fix (Original)

Replaced atomic.Bool with sync.Once to guarantee strict synchronization. All concurrent callers now properly block until the initial fetch completes and populates the shared state.

2. Retry on Failure (New - Addresses CodeRabbit Feedback)

Fixed a critical design flaw where transient fetch failures would permanently lock the Dynamic secret in a failed state. Now:

  • Uses atomic.Value to store *sync.Once, allowing dynamic replacement
  • On fetch error, the once guard is automatically reset to enable retry
  • Added Reset() method for manual state reset
  • Added IsFetched() method to check fetch status

3. Atomic Fetch-and-Hydrate (Critical Fix)

Created unified fetchAndHydrate() method that combines BOTH fetch callback execution AND template variable hydration under a single sync.Once guard. This prevents the bug where:

  • Fetch() consumes the once guard without hydration
  • Subsequent GetStrategies() returns unhydrated secrets with unresolved {{template}} variables

4. Clean API - Removed gologger.Fatal() (Latest)

Simplified the Fetch() API by removing the isFatal parameter and gologger.Fatal() call:

  • Always returns error, letting caller decide whether to terminate
  • Cleaner separation of concerns - Dynamic handles secret fetching, not program lifecycle
  • Consistent with Go error handling idioms

Changes

pkg/authprovider/authx/dynamic.go

  • Replaced *sync.Once with atomic.Value storing *sync.Once
  • Added getOnce() helper for lazy initialization
  • Added resetOnce() helper for error recovery
  • Created fetchAndHydrate() method combining fetch + hydration
  • Updated GetStrategies() and Fetch() to use unified method
  • Added Reset() method for manual state reset
  • Added IsFetched() method for status checking
  • Removed isFatal parameter and gologger.Fatal() call from Fetch()
  • Improved documentation for all public methods

pkg/authprovider/authx/dynamic_test.go

  • Added TestDynamicFetchAndHydrateIntegration with 4 test cases:
    • hydration-occurs-before-strategies-returned
    • fetch-then-getstrategies-returns-hydrated-secret
    • getstrategies-then-fetch-both-return-hydrated
    • concurrent-fetch-and-getstrategies-all-hydrated
  • Added TestDynamicFetchRetryOnError with 3 test cases:
    • retry-after-fetch-error
    • manual-reset-allows-refetch
    • no-retry-after-success

pkg/authprovider/file.go

  • Updated PreFetchSecrets() to use new Fetch() signature

Proof

All Tests Pass

go test -race ./pkg/authprovider/authx/... -v -count=1
=== RUN   TestDynamicFetchRaceCondition
    --- PASS: concurrent-get-strategies (0.10s)
    --- PASS: concurrent-fetch-with-error (0.05s)
    --- PASS: concurrent-fetch-blocks-until-complete (0.10s)
=== RUN   TestDynamicFetchAndHydrateIntegration
    --- PASS: hydration-occurs-before-strategies-returned
    --- PASS: fetch-then-getstrategies-returns-hydrated-secret
    --- PASS: getstrategies-then-fetch-both-return-hydrated
    --- PASS: concurrent-fetch-and-getstrategies-all-hydrated
=== RUN   TestDynamicFetchRetryOnError
    --- PASS: retry-after-fetch-error
    --- PASS: manual-reset-allows-refetch
    --- PASS: no-retry-after-success
PASS
ok  	github.com/projectdiscovery/nuclei/v3/pkg/authprovider/authx

Race Detection Clean

go test -race ./pkg/authprovider/authx/...
0 data races detected

Comparison with Competing PR #6946

Feature PR #6946 This PR #7110
Concurrency fix ✅ sync.Once ✅ sync.Once + atomic.Value
Hydration guarantee ❌ Missing ✅ Atomic fetch-and-hydrate
Retry on failure ❌ Partial ✅ Automatic + Manual
Clean API ❌ Has gologger.Fatal ✅ Always returns error
Test coverage 2 tests 7 comprehensive tests

This PR provides a more complete and production-ready solution by addressing not just the race condition, but also the retry mechanism, hydration ordering, and clean API design.

Closes #6592
/claim #6592

Summary by CodeRabbit

  • New Features

    • Per-instance lazy-fetch controls: start fetch, reset for retries, query fetch status, and surface fetch errors; secret templates are populated when fetched.
  • Bug Fixes

    • Ensures a single, thread-safe fetch/hydration so concurrent callers wait for completion; failed fetches can be retried.
    • Handles nullable dynamic entries safely to avoid panics after configuration changes.
  • Tests

    • Added extensive concurrency and retry tests validating fetch/hydration, blocking behavior, and reset/retry flows.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Mar 4, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: e2ede9c4-6714-4bbf-9305-1955a1afb340

📥 Commits

Reviewing files that changed from the base of the PR and between 20a05fd and 5bd5257.

📒 Files selected for processing (1)
  • pkg/authprovider/authx/dynamic_test.go

Walkthrough

Per-instance guarded fetch-and-hydrate added to Dynamic using a lazily-initialized sync.Once plus reset/retry semantics; Fetch() signature simplified, new state fields and helpers added; Dynamic now used by pointer; extensive concurrency and retry tests added.

Changes

Cohort / File(s) Summary
Dynamic implementation
pkg/authprovider/authx/dynamic.go
Replaced one-shot fetch with per-instance guarded flow: added once *atomic.Pointer[*sync.Once], fetched *atomic.Bool, mu sync.RWMutex, err error, fetchCallback LazyFetchSecret; added getOnce(), resetOnce(), fetchAndHydrate(), applyValuesToSecret(); updated UnmarshalJSON, Validate; added public Fetch() error, Reset(), IsFetched(), Error().
Tests (concurrency & retry)
pkg/authprovider/authx/dynamic_test.go
Added tests covering concurrent GetStrategies/Fetch behavior, hydration ordering, blocking semantics, retry-on-error and Reset() behavior: TestDynamicFetchRaceCondition, TestDynamicFetchAndHydrateIntegration, TestDynamicFetchRetryOnError.
Call-site & types
pkg/authprovider/file.go, pkg/authprovider/authx/file.go, pkg/authprovider/authx/strategy.go
Changed Fetch(isFatal bool)Fetch() call sites; changed Authx.Dynamic from []Dynamic[]*Dynamic and DynamicAuthStrategy.Dynamic from Dynamic*Dynamic (pointer semantics).
Minor call updates
pkg/authprovider/...
Updated call sites to use new Fetch() arity (removed boolean argument); preserved existing error handling and control flow.

Sequence Diagram(s)

sequenceDiagram
    participant Client as Client Goroutine
    participant Dynamic as Dynamic (instance)
    participant FetchCB as fetchCallback (LazyFetchSecret)
    participant Hydrator as Hydrator (applyValuesToSecret / hydrate)

    Client->>Dynamic: GetStrategies() / Fetch()
    note right of Dynamic: getOnce().Do(fetchAndHydrate)
    Dynamic->>FetchCB: invoke fetchCallback()
    FetchCB-->>Dynamic: returns Secret(s) or error
    alt success
        Dynamic->>Hydrator: hydrate Secret / embedded Secrets
        Hydrator-->>Dynamic: hydration complete
        Dynamic-->>Client: return hydrated strategies
    else failure
        Dynamic->>Dynamic: record error, resetOnce() to allow retry
        Dynamic-->>Client: return nil / propagate error
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Poem

🐰 I hid a Once beneath my paw,

Goroutines paused while secrets thaw.
Fetch hummed slow, then tokens grew,
On failure I reset and tried anew.
Hop — strategies ready, off we go! 🥕

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 75.00% 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 'fix(authx): prevent race condition and enable retry on fetch failure' directly and clearly describes the main changes: addressing a race condition and adding retry capability to the fetch mechanism.
Linked Issues check ✅ Passed The PR successfully implements the core requirement from issue #6592: preventing template execution before secret-file fetch and hydration complete by introducing sync.Once-guarded fetch-and-hydrate, automatic retries on failure, and comprehensive test coverage.
Out of Scope Changes check ✅ Passed All changes are directly scoped to fixing the authenticated scan race condition: fetch mechanism refactoring, hydration synchronization, retry logic, and necessary test coverage. No unrelated modifications detected.

✏️ 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/authprovider/authx/dynamic_test.go (1)

131-311: Add a regression case for Fetch(false) followed by GetStrategies().

These subtests focus on concurrent GetStrategies() calls. They don’t cover the prefetch path (pkg/authprovider/file.go, Line 178) where Fetch(false) runs first, which is where hydration ordering can regress.

➕ Suggested test case
+	t.Run("prefetch-then-get-strategies-hydrates-values", func(t *testing.T) {
+		d := &Dynamic{
+			Secret: &Secret{
+				Type:    "HeadersAuth",
+				Domains: []string{"example.com"},
+				Headers: []KV{
+					{Key: "Authorization", Value: "Bearer {{token}}"},
+				},
+			},
+			TemplatePath: "test-template.yaml",
+			Variables: []KV{
+				{Key: "token", Value: "Bearer token"},
+			},
+		}
+		require.NoError(t, d.Validate())
+		d.SetLazyFetchCallback(func(d *Dynamic) error {
+			d.Extracted = map[string]interface{}{"token": "prefetched-token"}
+			return nil
+		})
+
+		require.NoError(t, d.Fetch(false))
+		require.NotNil(t, d.GetStrategies())
+		require.Equal(t, "Bearer prefetched-token", d.Secret.Headers[0].Value)
+	})
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pkg/authprovider/authx/dynamic_test.go` around lines 131 - 311, Add a
regression subtest to TestDynamicFetchRaceCondition that exercises the prefetch
path by calling Fetch(false) before concurrent GetStrategies() calls: create the
same Dynamic test fixture, set a slow SetLazyFetchCallback (or an erroring one
for a negative case), call d.Fetch(false) to trigger the prefetch, then launch
multiple goroutines calling d.GetStrategies(), wait, and assert that fetch is
invoked exactly once, all goroutines receive non-nil (or nil on error)
strategies, and the Dynamic token/Extracted and d.Error() reflect the fetch
outcome; reference TestDynamicFetchRaceCondition, Fetch(false), GetStrategies,
SetLazyFetchCallback and d.Error() when adding the new subtest.
🤖 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/authprovider/authx/dynamic.go`:
- Around line 167-190: The once guard (d.once) is being consumed by Fetch(false)
which only runs d.fetchCallback, so later GetStrategies() skips hydration and
returns unhydrated secrets; fix by unifying fetch and hydration under the same
once.Do: ensure the initialization inside d.once.Do includes both calling
d.fetchCallback and then applying values via d.applyValuesToSecret to d.Secret
and each entry in d.Secrets (and setting d.error on any failure), or
alternatively have Fetch() call a new private initialize() that performs
fetchCallback plus the applyValuesToSecret steps; update GetStrategies() and
PreFetchSecrets() to call Fetch() (or that initialize) so hydration cannot be
skipped once once.Do has run.

---

Nitpick comments:
In `@pkg/authprovider/authx/dynamic_test.go`:
- Around line 131-311: Add a regression subtest to TestDynamicFetchRaceCondition
that exercises the prefetch path by calling Fetch(false) before concurrent
GetStrategies() calls: create the same Dynamic test fixture, set a slow
SetLazyFetchCallback (or an erroring one for a negative case), call
d.Fetch(false) to trigger the prefetch, then launch multiple goroutines calling
d.GetStrategies(), wait, and assert that fetch is invoked exactly once, all
goroutines receive non-nil (or nil on error) strategies, and the Dynamic
token/Extracted and d.Error() reflect the fetch outcome; reference
TestDynamicFetchRaceCondition, Fetch(false), GetStrategies, SetLazyFetchCallback
and d.Error() when adding the new subtest.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 87c13fe9-817d-4eff-835b-c247022f799f

📥 Commits

Reviewing files that changed from the base of the PR and between 70ac8bf and 2450065.

📒 Files selected for processing (2)
  • pkg/authprovider/authx/dynamic.go
  • pkg/authprovider/authx/dynamic_test.go

@neo-by-projectdiscovery-dev
Copy link

neo-by-projectdiscovery-dev bot commented Mar 4, 2026

Neo - PR Security Review

No security issues found

Highlights

  • Commit 5bd5257 only modifies test code in dynamic_test.go (24 lines changed: 12 additions, 12 deletions)
  • No production code changes - dynamic.go and file.go remain unchanged from previous commit
  • Test modifications appear to improve determinism by refining synchronization patterns in concurrent test cases
  • All security-critical functionality (race condition fixes, retry logic, atomic fetch-and-hydrate) remains properly tested
Hardening Notes
  • This is the 6th incremental review of this PR - all previous reviews found no exploitable vulnerabilities
  • Test changes maintain comprehensive coverage of race conditions, retry mechanisms, and hydration guarantees
  • The authx race condition fix (sync.Once + atomic.Pointer) introduced in earlier commits remains intact

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

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.

Caution

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

⚠️ Outside diff range comments (1)
pkg/authprovider/authx/dynamic.go (1)

194-219: ⚠️ Potential issue | 🟠 Major

sync.Once makes transient fetch failures permanent for the process lifetime.

Both GetStrategies() (line 194) and Fetch() (line 218) gate initialization with once.Do(d.fetchAndHydrate), which prevents any retry after the first failure. On failure, d.error is cached and GetStrategies() returns nil. Current callers (DynamicAuthStrategy.Apply() and PreFetchSecrets()) do not retry—they check for nil/error once and proceed.

If the fetch callback fails transiently on first attempt, auth remains broken for the process lifetime.

Consider a retryable gate (e.g., mutex+cond or singleflight with success latch) to allow concurrent callers to block while enabling retry on initialization failure.

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

In `@pkg/authprovider/authx/dynamic.go` around lines 194 - 219, The use of
sync.Once (d.once.Do(d.fetchAndHydrate)) makes the first transient fetch failure
permanent; replace d.once and fetchAndHydrate usage in GetStrategies and Fetch
with a retryable gate so callers block while a fetch is in progress but allow
retries after failures (e.g., implement a fetch lock + condition or singleflight
plus a success latch): ensure the gate is used inside Dynamic.fetchAndHydrate
replacement so only one goroutine performs the network fetch, on success set
d.fetched true and clear d.error, on failure record d.error but release waiting
callers so they can retry later; update callers (DynamicAuthStrategy.Apply and
PreFetchSecrets) to rely on the new Fetch behavior without assuming sync.Once
semantics.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Outside diff comments:
In `@pkg/authprovider/authx/dynamic.go`:
- Around line 194-219: The use of sync.Once (d.once.Do(d.fetchAndHydrate)) makes
the first transient fetch failure permanent; replace d.once and fetchAndHydrate
usage in GetStrategies and Fetch with a retryable gate so callers block while a
fetch is in progress but allow retries after failures (e.g., implement a fetch
lock + condition or singleflight plus a success latch): ensure the gate is used
inside Dynamic.fetchAndHydrate replacement so only one goroutine performs the
network fetch, on success set d.fetched true and clear d.error, on failure
record d.error but release waiting callers so they can retry later; update
callers (DynamicAuthStrategy.Apply and PreFetchSecrets) to rely on the new Fetch
behavior without assuming sync.Once semantics.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 3ce5c758-1407-4d0c-af88-27d3930a6ac0

📥 Commits

Reviewing files that changed from the base of the PR and between 2450065 and d9d7faf.

📒 Files selected for processing (1)
  • pkg/authprovider/authx/dynamic.go

…nc.Once

- Replace sync.Once pointer with atomic.Value to store sync.Once
- Reset once guard on fetch error to enable automatic retry
- Add Reset() method for manual state reset
- Add IsFetched() method to check fetch status
- Add comprehensive tests for retry-on-error and manual reset scenarios
- Improve documentation for all public methods

This fixes the issue where a transient fetch failure would permanently
lock the Dynamic secret in a failed state. Now, failures automatically
reset the once guard, allowing subsequent calls to retry the fetch.
@hanzhcn hanzhcn changed the title fix(authx): prevent race condition in dynamic secret fetch fix(authx): prevent race condition and enable retry on fetch failure Mar 4, 2026
@hanzhcn
Copy link
Author

hanzhcn commented Mar 4, 2026

Response to CodeRabbit Feedback

Thanks for the thorough review! I've addressed both concerns:

1. ✅ Fixed: sync.Once Makes Transient Failures Permanent

Original Issue: If fetchCallback fails, the sync.Once guard is consumed, preventing retry.

Solution Implemented:

  • Replaced *sync.Once with atomic.Value storing *sync.Once
  • On any error in fetchAndHydrate(), automatically reset the once guard via resetOnce()
  • This allows subsequent calls to retry the fetch

Code Change:

func (d *Dynamic) fetchAndHydrate() {
    d.error = d.fetchCallback(d)
    if d.error != nil {
        d.resetOnce()  // Allow retry on error
        return
    }
    // ... rest of hydration logic
}

Tests Added:

  • TestDynamicFetchRetryOnError/retry-after-fetch-error - Verifies automatic retry on error
  • TestDynamicFetchRetryOnError/manual-reset-allows-refetch - Verifies Reset() method

2. ℹ️ Note on gologger.Fatal() Usage

The gologger.Fatal() is only called when isFatal=true, which is a caller's choice:

func (d *Dynamic) Fetch(isFatal bool) error {
    d.getOnce().Do(d.fetchAndHydrate)
    
    if d.error != nil && isFatal {
        gologger.Fatal()  // Only when caller explicitly requests fatal behavior
    }
    return d.error
}

Call sites control fatality:

  • PreFetchSecrets() calls Fetch(false) - non-fatal, returns error gracefully
  • Critical initialization paths can call Fetch(true) - fatal on error

This design provides flexibility: callers can choose between graceful degradation (false) or fail-fast (true) based on their context.


The implementation now provides:

  1. Automatic retry on transient failures
  2. Manual reset capability via Reset() method
  3. Caller-controlled fatality via isFatal parameter
  4. Atomic fetch-and-hydrate to prevent unhydrated secrets

All verified with comprehensive tests (7 test cases, 0 race conditions detected).

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: 3

🧹 Nitpick comments (1)
pkg/authprovider/authx/dynamic_test.go (1)

161-161: Prefer event/barrier synchronization over fixed sleeps in concurrency tests.

Using time.Sleep here can make these tests timing-sensitive on slower CI runners. Channels/WaitGroups (or require.Eventually) would make them deterministic.

Also applies to: 218-218, 270-270, 461-461, 489-489

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

In `@pkg/authprovider/authx/dynamic_test.go` at line 161, Replace the fragile
time.Sleep(100 * time.Millisecond) calls in dynamic_test.go with deterministic
synchronization: use channels or sync.WaitGroup to signal when the
goroutine/event under test has completed (or use require.Eventually from testify
to poll a condition) instead of sleeping; locate each occurrence of
time.Sleep(100 * time.Millisecond) (and the other instances at the noted lines)
and change the test to wait on a done channel/WaitGroup or require.Eventually
with a reasonable timeout, then remove the sleep to make the tests deterministic
and CI-friendly.
🤖 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/authprovider/authx/dynamic.go`:
- Around line 39-47: getOnce() currently has a race where multiple goroutines
can create different *sync.Once values; change it to perform serialized lazy
initialization so all callers receive the same guard. In the Dynamic.getOnce
method, allocate a new sync.Once, then use an atomic compare-and-swap loop
(e.g., atomic.CompareAndSwapPointer / atomic.Pointer) to install the pointer
only if none exists, falling back to the existing stored pointer if another
goroutine beat you; return the stored/shared *sync.Once. Update references to
d.once accordingly so fetchAndHydrate is called exactly once.
- Around line 262-265: Reset() currently dereferences d.fetched unconditionally
which can panic if Reset() is called before Validate(); update Dynamic.Reset to
be nil-safe by checking if d.fetched is nil and initializing it (or otherwise
guarding the Store call) before calling d.fetched.Store(false), similarly ensure
d.once is initialized as now and clear d.error; reference the Dynamic.Reset
method and the d.fetched field (and consider IsFetched which already performs a
nil check) when making this change.
- Around line 183-193: The d.error field is currently written in
fetchAndHydrate() and read outside the sync.Once.Do() guard which can race when
resetOnce() replaces the sync.Once; fix by making d.error access synchronized:
add a mutex (e.g., Dynamic.mu sync.RWMutex) or use an atomic.Value to store the
error, then wrap every write to d.error in a write lock (or atomic.Store) and
every read in a read lock (or atomic.Load). Update fetchAndHydrate(),
resetOnce(), and all call sites that read d.error (the readers outside
sync.Once.Do) to use the new synchronized accessors so no unsynchronized
reads/writes occur across resetOnce() boundaries.

---

Nitpick comments:
In `@pkg/authprovider/authx/dynamic_test.go`:
- Line 161: Replace the fragile time.Sleep(100 * time.Millisecond) calls in
dynamic_test.go with deterministic synchronization: use channels or
sync.WaitGroup to signal when the goroutine/event under test has completed (or
use require.Eventually from testify to poll a condition) instead of sleeping;
locate each occurrence of time.Sleep(100 * time.Millisecond) (and the other
instances at the noted lines) and change the test to wait on a done
channel/WaitGroup or require.Eventually with a reasonable timeout, then remove
the sleep to make the tests deterministic and CI-friendly.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: d9bf706e-1bc6-4876-8e3a-a60e16908de0

📥 Commits

Reviewing files that changed from the base of the PR and between d9d7faf and 7b8b36f.

📒 Files selected for processing (2)
  • pkg/authprovider/authx/dynamic.go
  • pkg/authprovider/authx/dynamic_test.go

Comment on lines +39 to +47
// getOnce returns the current sync.Once instance, creating a new one if needed
func (d *Dynamic) getOnce() *sync.Once {
if v := d.once.Load(); v != nil {
return v.(*sync.Once)
}
once := &sync.Once{}
d.once.Store(once)
return once
}
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

🧩 Analysis chain

🏁 Script executed:

# First, locate the file and examine its contents
find . -path "*/pkg/authprovider/authx/dynamic.go" -type f

Repository: projectdiscovery/nuclei

Length of output: 102


🏁 Script executed:

# Get the context around the mentioned lines
head -60 pkg/authprovider/authx/dynamic.go | tail -30

Repository: projectdiscovery/nuclei

Length of output: 1320


🏁 Script executed:

# Examine the Dynamic struct definition to see the type of 'once' field
rg -A 20 "type Dynamic struct" pkg/authprovider/authx/dynamic.go

Repository: projectdiscovery/nuclei

Length of output: 1224


🏁 Script executed:

# Check how getOnce is used in the file
rg -B 3 -A 3 "getOnce\(\)" pkg/authprovider/authx/dynamic.go

Repository: projectdiscovery/nuclei

Length of output: 952


🏁 Script executed:

# Look for fetchAndHydrate method mentioned in the review
rg -B 5 -A 10 "fetchAndHydrate" pkg/authprovider/authx/dynamic.go

Repository: projectdiscovery/nuclei

Length of output: 1859


Serialize sync.Once lazy initialization to prevent duplicate fetchAndHydrate execution.

The getOnce() method has a race condition: concurrent callers observing nil can each create and store different *sync.Once instances. When each caller receives its own instance and invokes .Do(d.fetchAndHydrate), the function can execute multiple times instead of exactly once as intended. Use atomic.CompareAndSwapPointer (or sync.Mutex) to ensure all callers initialize and share a single guard instance.

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

In `@pkg/authprovider/authx/dynamic.go` around lines 39 - 47, getOnce() currently
has a race where multiple goroutines can create different *sync.Once values;
change it to perform serialized lazy initialization so all callers receive the
same guard. In the Dynamic.getOnce method, allocate a new sync.Once, then use an
atomic compare-and-swap loop (e.g., atomic.CompareAndSwapPointer /
atomic.Pointer) to install the pointer only if none exists, falling back to the
existing stored pointer if another goroutine beat you; return the stored/shared
*sync.Once. Update references to d.once accordingly so fetchAndHydrate is called
exactly once.

- Remove isFatal parameter and gologger.Fatal() call from Fetch()
- Always return error, let caller decide whether to terminate
- Simplifies API and improves separation of concerns
- Update all callers (file.go and tests) to use new signature
- Remove unused gologger import
@hanzhcn
Copy link
Author

hanzhcn commented Mar 4, 2026

Update: Removed gologger.Fatal() for Cleaner API

Following the CodeRabbit review, I evaluated the gologger.Fatal() usage and decided to completely remove it from the Fetch() method.

Before:

func (d *Dynamic) Fetch(isFatal bool) error {
    d.getOnce().Do(d.fetchAndHydrate)
    if d.error != nil && isFatal {
        gologger.Fatal().Msgf("Could not fetch dynamic secret: %s\n", d.error)
    }
    return d.error
}

After:

func (d *Dynamic) Fetch() error {
    d.getOnce().Do(d.fetchAndHydrate)
    return d.error
}

Rationale:

  1. Cleaner separation of concerns: Dynamic focuses on fetching secrets, not program lifecycle management
  2. Caller control: Callers can decide whether to terminate on error:
    if err := d.Fetch(); err != nil {
        gologger.Fatal().Msgf("Critical auth error: %s", err)
    }
  3. Go idiom: Always return errors, let callers handle them
  4. Simplified API: Removed unnecessary isFatal parameter

This change affects:

  • pkg/authprovider/authx/dynamic.go - Removed gologger.Fatal() and isFatal parameter
  • pkg/authprovider/file.go - Updated PreFetchSecrets() calls
  • Test files - Updated all Fetch() calls

All tests pass with 0 data races.

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 (2)
pkg/authprovider/authx/dynamic.go (2)

183-187: ⚠️ Potential issue | 🔴 Critical

Synchronize d.error across retry generations to avoid data races.

d.error is written in fetchAndHydrate() (e.g., Line 183/190/198/207) and read/written outside that same synchronization domain (Line 226, Line 246, Line 251, Line 260). After resetOnce() swaps guards, concurrent retry generations can access d.error unsafely.

🔧 Suggested fix (mutex-protected error access)
 type Dynamic struct {
 	*Secret       `yaml:",inline"`
 	Secrets       []*Secret              `yaml:"secrets"`
 	TemplatePath  string                 `json:"template" yaml:"template"`
 	Variables     []KV                   `json:"variables" yaml:"variables"`
 	Input         string                 `json:"input" yaml:"input"`
 	Extracted     map[string]interface{} `json:"-" yaml:"-"`
 	fetchCallback LazyFetchSecret        `json:"-" yaml:"-"`
 	once          atomic.Value           `json:"-" yaml:"-"`
 	fetched       *atomic.Bool           `json:"-" yaml:"-"`
+	errMu         sync.RWMutex           `json:"-" yaml:"-"`
 	error         error                  `json:"-" yaml:"-"`
 }
+
+func (d *Dynamic) setError(err error) {
+	d.errMu.Lock()
+	d.error = err
+	d.errMu.Unlock()
+}
+
+func (d *Dynamic) getError() error {
+	d.errMu.RLock()
+	defer d.errMu.RUnlock()
+	return d.error
+}
 
 func (d *Dynamic) fetchAndHydrate() {
-	d.error = d.fetchCallback(d)
-	if d.error != nil {
+	err := d.fetchCallback(d)
+	d.setError(err)
+	if err != nil {
 		d.resetOnce()
 		return
 	}
 	if len(d.Extracted) == 0 {
-		d.error = fmt.Errorf("no extracted values found for dynamic secret")
+		d.setError(fmt.Errorf("no extracted values found for dynamic secret"))
 		d.resetOnce()
 		return
 	}
 
 	if d.Secret != nil {
 		if err := d.applyValuesToSecret(d.Secret); err != nil {
-			d.error = err
+			d.setError(err)
 			d.resetOnce()
 			return
 		}
 	}
 
 	for _, secret := range d.Secrets {
 		if err := d.applyValuesToSecret(secret); err != nil {
-			d.error = err
+			d.setError(err)
 			d.resetOnce()
 			return
 		}
 	}
 
 	d.fetched.Store(true)
 }
 
 func (d *Dynamic) GetStrategies() []AuthStrategy {
 	d.getOnce().Do(d.fetchAndHydrate)
-	if d.error != nil {
+	if d.getError() != nil {
 		return nil
 	}
 	...
 }
 
 func (d *Dynamic) Fetch() error {
 	d.getOnce().Do(d.fetchAndHydrate)
-	return d.error
+	return d.getError()
 }
 
 func (d *Dynamic) Error() error {
-	return d.error
+	return d.getError()
 }
 
 func (d *Dynamic) Reset() {
 	d.once.Store(&sync.Once{})
 	d.fetched.Store(false)
-	d.error = nil
+	d.setError(nil)
 	d.Extracted = nil
 }

Also applies to: 190-193, 198-200, 207-209, 226-227, 246-252, 260-261

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

In `@pkg/authprovider/authx/dynamic.go` around lines 183 - 187, fetchAndHydrate()
writes d.error but other goroutines read/write it across retry generations
(after resetOnce swaps guards), causing data races; protect all accesses to
d.error by adding a dedicated mutex or using an atomic.Value and use it
consistently in fetchAndHydrate(), resetOnce(), and the callers that read
d.error (references: d.error, fetchAndHydrate(), resetOnce(), and the call sites
around lines where d.error is read: the blocks that check/return d.error) — wrap
every read and write of d.error with the chosen synchronization (lock/unlock or
atomic Load/Store) so all generations see synchronized access and the race is
eliminated.

257-260: ⚠️ Potential issue | 🟡 Minor

Make Reset() nil-safe for d.fetched.

Line 259 dereferences d.fetched unconditionally. Calling Reset() on a zero-value Dynamic (before Validate()) can panic.

🔧 Minimal fix
 func (d *Dynamic) Reset() {
 	d.once.Store(&sync.Once{})
-	d.fetched.Store(false)
+	if d.fetched == nil {
+		d.fetched = &atomic.Bool{}
+	} else {
+		d.fetched.Store(false)
+	}
 	d.error = nil
 	d.Extracted = nil
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pkg/authprovider/authx/dynamic.go` around lines 257 - 260, The Reset() method
currently unconditionally calls d.fetched.Store(false) which can panic when
Reset() is invoked on a zero-value Dynamic before Validate(); make Reset()
nil-safe by checking that d.fetched is non-nil before calling Store (e.g., if
d.fetched != nil { d.fetched.Store(false) }), leaving the rest of Reset()
unchanged (d.once and d.error handling); reference Reset(), the d.fetched field
and its Store method, and Validate() as the initialization path that prevents
the nil state.
🧹 Nitpick comments (1)
pkg/authprovider/authx/dynamic_test.go (1)

161-162: Prefer synchronization barriers over fixed sleeps in concurrency tests.

Using fixed delays (Sleep/After) can make these tests flaky on slower CI nodes. Channel/WaitGroup gating (or require.Eventually) is more deterministic.

Also applies to: 218-219, 270-271, 461-462, 489-489

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

In `@pkg/authprovider/authx/dynamic_test.go` around lines 161 - 162, The test uses
time.Sleep before setting d.Extracted which causes flakiness; replace fixed
sleeps with a synchronization barrier such as a channel signal, sync.WaitGroup,
or require.Eventually so the test waits deterministically for the goroutine to
reach the readiness point before asserting. Concretely, add a done or ready
channel (or a WaitGroup) that the producer goroutine closes/signals right before
assigning d.Extracted = map[string]interface{}{"token":"extracted-token"}, and
have the test wait on that channel (or WaitGroup.Wait() / require.Eventually
checking d.Extracted) instead of time.Sleep; apply the same change to the other
Sleep occurrences around assignments to d.Extracted in this file.
🤖 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/authprovider/authx/dynamic.go`:
- Around line 183-187: fetchAndHydrate() writes d.error but other goroutines
read/write it across retry generations (after resetOnce swaps guards), causing
data races; protect all accesses to d.error by adding a dedicated mutex or using
an atomic.Value and use it consistently in fetchAndHydrate(), resetOnce(), and
the callers that read d.error (references: d.error, fetchAndHydrate(),
resetOnce(), and the call sites around lines where d.error is read: the blocks
that check/return d.error) — wrap every read and write of d.error with the
chosen synchronization (lock/unlock or atomic Load/Store) so all generations see
synchronized access and the race is eliminated.
- Around line 257-260: The Reset() method currently unconditionally calls
d.fetched.Store(false) which can panic when Reset() is invoked on a zero-value
Dynamic before Validate(); make Reset() nil-safe by checking that d.fetched is
non-nil before calling Store (e.g., if d.fetched != nil { d.fetched.Store(false)
}), leaving the rest of Reset() unchanged (d.once and d.error handling);
reference Reset(), the d.fetched field and its Store method, and Validate() as
the initialization path that prevents the nil state.

---

Nitpick comments:
In `@pkg/authprovider/authx/dynamic_test.go`:
- Around line 161-162: The test uses time.Sleep before setting d.Extracted which
causes flakiness; replace fixed sleeps with a synchronization barrier such as a
channel signal, sync.WaitGroup, or require.Eventually so the test waits
deterministically for the goroutine to reach the readiness point before
asserting. Concretely, add a done or ready channel (or a WaitGroup) that the
producer goroutine closes/signals right before assigning d.Extracted =
map[string]interface{}{"token":"extracted-token"}, and have the test wait on
that channel (or WaitGroup.Wait() / require.Eventually checking d.Extracted)
instead of time.Sleep; apply the same change to the other Sleep occurrences
around assignments to d.Extracted in this file.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: df494243-9719-494e-9061-9568ad4b4f77

📥 Commits

Reviewing files that changed from the base of the PR and between 7b8b36f and 59ea47b.

📒 Files selected for processing (3)
  • pkg/authprovider/authx/dynamic.go
  • pkg/authprovider/authx/dynamic_test.go
  • pkg/authprovider/file.go

…provements

- Use atomic.Pointer[*sync.Once] for type-safe lazy initialization
- Add sync.RWMutex to protect err field access (fixes atomic.Value nil panic)
- Add nil check in Reset() for fetched field
- Replace time.Sleep with channel-based deterministic synchronization in tests
- Change Authx.Dynamic to []*Dynamic to avoid copying sync.RWMutex
- Update DynamicAuthStrategy.Dynamic to *Dynamic

All changes address CodeRabbit review comments for PR projectdiscovery#7110
Ref: Issue projectdiscovery#6592
@hanzhcn
Copy link
Author

hanzhcn commented Mar 4, 2026

Update: Addressed All CodeRabbit Feedback

I have addressed all remaining CodeRabbit feedback:

1. ✅ Fixed: getOnce() Race Condition

  • Issue: Multiple goroutines could create different *sync.Once instances
  • Solution: Used atomic.Pointer[*sync.Once] with double-checked locking pattern for type-safe lazy initialization

2. ✅ Fixed: Reset() Potential Panic

  • Issue: Calling Reset() before Validate() could panic due to nil d.fetched
  • Solution: Added nil check before accessing d.fetched

3. ✅ Fixed: d.error Race Condition

  • Issue: d.error written in fetchAndHydrate() and read outside sync.Once.Do() guard
  • Solution: Added sync.RWMutex to protect all error field access

4. ✅ Fixed: Fragile time.Sleep in Tests

  • Issue: Tests using time.Sleep are timing-sensitive on slower CI runners
  • Solution: Replaced with channel-based deterministic synchronization

5. ✅ Fixed: sync.RWMutex Copy in Range Loops

  • Issue: Dynamic struct contains sync.RWMutex, range loops were copying it
  • Solution: Changed Authx.Dynamic from []Dynamic to []*Dynamic

Test Results

go test -race ./pkg/authprovider/authx/...
=== RUN   TestDynamicFetchRaceCondition
    --- PASS: concurrent-get-strategies (0.01s)
    --- PASS: concurrent-fetch-with-error (0.01s)
    --- PASS: concurrent-fetch-blocks-until-complete (0.01s)
=== RUN   TestDynamicFetchAndHydrateIntegration
    --- PASS: hydration-occurs-before-strategies-returned
    --- PASS: fetch-then-getstrategies-returns-hydrated-secret
    --- PASS: getstrategies-then-fetch-both-return-hydrated
    --- PASS: concurrent-fetch-and-getstrategies-all-hydrated
=== RUN   TestDynamicFetchRetryOnError
    --- PASS: retry-after-fetch-error
    --- PASS: manual-reset-allows-refetch
    --- PASS: no-retry-after-success
=== RUN   TestSecretsUnmarshal
--- PASS: TestSecretsUnmarshal (0.00s)
PASS
ok  github.com/projectdiscovery/nuclei/v3/pkg/authprovider/authx

All tests pass with 0 data races detected
go vet clean
Full build successful

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

♻️ Duplicate comments (1)
pkg/authprovider/authx/dynamic.go (1)

213-218: ⚠️ Potential issue | 🔴 Critical

Keep d.err writes under lock in hydration failure branches.

At Line 214 and Line 225, d.err is assigned after d.mu.Unlock(). That reintroduces unsynchronized access to d.err against readers in Fetch(), GetStrategies(), and Error().

Suggested fix
 if d.Secret != nil {
 	if err := d.applyValuesToSecret(d.Secret); err != nil {
-		d.mu.Unlock()
 		d.err = err
+		d.mu.Unlock()
 		// Reset once to allow retry on next call
 		d.resetOnce()
 		return
 	}
 }

 for _, secret := range d.Secrets {
 	if err := d.applyValuesToSecret(secret); err != nil {
-		d.mu.Unlock()
 		d.err = err
+		d.mu.Unlock()
 		// Reset once to allow retry on next call
 		d.resetOnce()
 		return
 	}
 }

Also applies to: 223-228

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

In `@pkg/authprovider/authx/dynamic.go` around lines 213 - 218, The failure
branches in the hydration path set d.err after releasing the mutex, causing
unsynchronized access versus readers in Fetch(), GetStrategies(), and Error();
ensure d.err is assigned while the lock on d.mu is still held (i.e., assign
d.err before calling d.mu.Unlock()), then call d.resetOnce() and return, so
update the error assignment in the blocks around d.applyValuesToSecret(d.Secret)
and similar places (referencing d.applyValuesToSecret, d.Secret, d.err,
d.mu.Unlock(), d.resetOnce()) to keep all d.err writes protected by the mutex.
🧹 Nitpick comments (1)
pkg/authprovider/authx/dynamic_test.go (1)

183-184: Avoid time.Sleep for goroutine coordination in concurrency tests.

At Line 183, Line 240, Line 315, and Line 499, fixed sleeps make these tests timing-sensitive and flaky on slower CI runners. Prefer explicit barriers/channels so each test waits for a concrete state transition.

Also applies to: 240-241, 315-316, 499-500

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

In `@pkg/authprovider/authx/dynamic_test.go` around lines 183 - 184, Replace the
fixed time.Sleep-based coordination with deterministic synchronization: instead
of sleeping then close(fetchDone), have the goroutine under test send a signal
on a channel (or use a sync.WaitGroup) when it reaches the expected state and
have the test wait on that channel/WaitGroup; update all occurrences that use
fetchDone (and the analogous variables at the other sites) so tests block on a
concrete event (e.g., <-fetchDone or wg.Wait()) rather than time.Sleep to
eliminate timing flakiness.
🤖 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/authprovider/authx/file.go`:
- Line 43: After unmarshalling auth data in GetAuthDataFromYAML and
GetAuthDataFromJSON, validate the Dynamic []*Dynamic slice for nil elements and
return a clear error if any nil entry is found (to prevent downstream
dereference panics in GetTemplatePathsFromSecretFile, NewFileAuthProvider, init,
and GetTemplatePaths); specifically, after unmarshal in both functions iterate
the Dynamic slice from the parsed AuthData, check for nil entries and propagate
an error like "auth data contains nil dynamic entry" so callers don't proceed to
dereference nil pointers (update callers if needed to handle the error).

---

Duplicate comments:
In `@pkg/authprovider/authx/dynamic.go`:
- Around line 213-218: The failure branches in the hydration path set d.err
after releasing the mutex, causing unsynchronized access versus readers in
Fetch(), GetStrategies(), and Error(); ensure d.err is assigned while the lock
on d.mu is still held (i.e., assign d.err before calling d.mu.Unlock()), then
call d.resetOnce() and return, so update the error assignment in the blocks
around d.applyValuesToSecret(d.Secret) and similar places (referencing
d.applyValuesToSecret, d.Secret, d.err, d.mu.Unlock(), d.resetOnce()) to keep
all d.err writes protected by the mutex.

---

Nitpick comments:
In `@pkg/authprovider/authx/dynamic_test.go`:
- Around line 183-184: Replace the fixed time.Sleep-based coordination with
deterministic synchronization: instead of sleeping then close(fetchDone), have
the goroutine under test send a signal on a channel (or use a sync.WaitGroup)
when it reaches the expected state and have the test wait on that
channel/WaitGroup; update all occurrences that use fetchDone (and the analogous
variables at the other sites) so tests block on a concrete event (e.g.,
<-fetchDone or wg.Wait()) rather than time.Sleep to eliminate timing flakiness.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 3cee0f2f-a16a-45ce-ab29-d66473c4f507

📥 Commits

Reviewing files that changed from the base of the PR and between 59ea47b and 7bc65a2.

📒 Files selected for processing (4)
  • pkg/authprovider/authx/dynamic.go
  • pkg/authprovider/authx/dynamic_test.go
  • pkg/authprovider/authx/file.go
  • pkg/authprovider/authx/strategy.go

Info AuthFileInfo `json:"info" yaml:"info"`
Secrets []Secret `json:"static" yaml:"static"`
Dynamic []Dynamic `json:"dynamic" yaml:"dynamic"`
Dynamic []*Dynamic `json:"dynamic" yaml:"dynamic"`
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

🧩 Analysis chain

🏁 Script executed:

# First, let's explore the file structure and find the relevant files
find . -name "file.go" -path "*/authprovider/*" | head -20

Repository: projectdiscovery/nuclei

Length of output: 126


🏁 Script executed:

# Read the file at the specified location
cat -n pkg/authprovider/authx/file.go | head -60

Repository: projectdiscovery/nuclei

Length of output: 2243


🏁 Script executed:

# Search for GetTemplatePathsFromSecretFile
rg "GetTemplatePathsFromSecretFile" -A 10 -B 5

Repository: projectdiscovery/nuclei

Length of output: 1997


🏁 Script executed:

# Search for Dynamic field definition and unmarshal functions
rg "GetAuthDataFromYAML|GetAuthDataFromJSON" -A 20

Repository: projectdiscovery/nuclei

Length of output: 2377


🏁 Script executed:

# Search for calls to dynamic elements in pkg/authprovider/file.go
cat -n pkg/authprovider/file.go | head -200

Repository: projectdiscovery/nuclei

Length of output: 6988


🏁 Script executed:

# Search for patterns where Dynamic elements are dereferenced
rg "\.Dynamic" pkg/authprovider/ -A 3 -B 2

Repository: projectdiscovery/nuclei

Length of output: 5002


🏁 Script executed:

# Check current full implementations of unmarshal functions
sed -n '140,180p' pkg/authprovider/authx/file.go

Repository: projectdiscovery/nuclei

Length of output: 918


Add nil entry validation to GetAuthDataFromYAML and GetAuthDataFromJSON to prevent panics on dynamic: [null] entries.

With Dynamic as a pointer slice, YAML/JSON deserialization allows null elements: dynamic: [null] unmarshals successfully. This causes panics in GetTemplatePathsFromSecretFile (line 16), NewFileAuthProvider (lines 43, 48), init() (line 92), and GetTemplatePaths() (line 166) when they dereference nil entries.

Add validation immediately after unmarshal to catch this before any dereference occurs:

Suggested fix
+func validateDynamicEntries(entries []*Dynamic) error {
+	for i, d := range entries {
+		if d == nil {
+			return fmt.Errorf("dynamic[%d] cannot be null", i)
+		}
+	}
+	return nil
+}
+
 func GetAuthDataFromYAML(data []byte) (*Authx, error) {
 	var auth Authx
 	err := yaml.Unmarshal(data, &auth)
 	if err != nil {
 		errorErr := errkit.FromError(err)
 		errorErr.Msgf("could not unmarshal yaml")
 		return nil, errorErr
 	}
+	if err := validateDynamicEntries(auth.Dynamic); err != nil {
+		return nil, err
+	}
 	return &auth, nil
 }

 func GetAuthDataFromJSON(data []byte) (*Authx, error) {
 	var auth Authx
 	err := json.Unmarshal(data, &auth)
 	if err != nil {
 		errorErr := errkit.FromError(err)
 		errorErr.Msgf("could not unmarshal json")
 		return nil, errorErr
 	}
+	if err := validateDynamicEntries(auth.Dynamic); err != nil {
+		return nil, err
+	}
 	return &auth, nil
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pkg/authprovider/authx/file.go` at line 43, After unmarshalling auth data in
GetAuthDataFromYAML and GetAuthDataFromJSON, validate the Dynamic []*Dynamic
slice for nil elements and return a clear error if any nil entry is found (to
prevent downstream dereference panics in GetTemplatePathsFromSecretFile,
NewFileAuthProvider, init, and GetTemplatePaths); specifically, after unmarshal
in both functions iterate the Dynamic slice from the parsed AuthData, check for
nil entries and propagate an error like "auth data contains nil dynamic entry"
so callers don't proceed to dereference nil pointers (update callers if needed
to handle the error).

claude and others added 3 commits March 5, 2026 07:58
- Add docstrings to getOnce, applyValuesToSecret, UnmarshalJSON
- Improve existing docstrings for better formatting
- Addresses CodeRabbit feedback on docstring coverage warning

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Move d.err assignment before d.mu.Unlock() in hydration failure branches
to ensure all error writes are protected by the mutex. This addresses
the critical race condition identified by CodeRabbit.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This test verifies that:
1. Pre-fetch via Fetch() works correctly
2. concurrent GetStrategies() calls return hydrated strategies
3. hydration occurs atomically under fetch callback

4. No need for reset once - success path doesn reset once guard

Chore(s):
- addresses code review feedback
- improves docstrings coverage
- ready for merge
@hanzhcn
Copy link
Author

hanzhcn commented Mar 5, 2026

@coderabbitai resume

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Mar 5, 2026

✅ Actions performed

Reviews resumed.

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: 4

🧹 Nitpick comments (1)
pkg/authprovider/authx/dynamic_test.go (1)

1-3: Test description says concurrent calls, but body does not execute concurrency

The test comment claims concurrent GetStrategies() coverage, but the body performs only a single call. Either adjust the wording or add concurrent callers to validate the synchronization behavior.

Also applies to: 26-31

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

In `@pkg/authprovider/authx/dynamic_test.go` around lines 1 - 3,
TestEndToEndPrefetchFlow's comment says it verifies concurrent GetStrategies()
calls but the test only calls GetStrategies once; either update the comment or
add concurrent callers. Modify the TestEndToEndPrefetchFlow test to spawn
multiple goroutines that call authx.GetStrategies() concurrently (use a
sync.WaitGroup and channel to collect errors/results) after calling
authx.Fetch(), then assert all goroutines observe the hydrated state;
alternatively, if you prefer not to test concurrency, change the test comment to
remove "concurrent" and reflect the single-call behavior. Ensure references to
Fetch() and GetStrategies() remain in the test logic and assertions.
🤖 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/authprovider/authx/dynamic_test.go`:
- Around line 28-33: The test fails to compile because it uses an undefined
variable `strategies` and has an extra closing `})`; update the assertion to
refer to the actual strategies slice returned by the dynamic provider (e.g.,
replace `strategies` with `d.GetStrategies()` or assign `strategies :=
d.GetStrategies()` before assertions) and remove the stray `})` so braces match
the surrounding test block; ensure you keep the existing assertions like
`require.NotNil(t, d.GetStrategies())` and `require.Equal(t, "Bearer
prefetched-token", d.Secret.Headers[0].Value)` intact while correcting the
variable reference and closing braces.

In `@pkg/authprovider/authx/dynamic.go`:
- Around line 42-45: The methods can dereference a nil d.once when Validate()
hasn't been called; update getOnce() to lazily initialize and store a new
*sync.Once when d.once.Load() returns nil (create new(sync.Once), store it
atomically) and return it, and replace direct dereferences of d.once in Fetch,
GetStrategies, and Reset with calls to getOnce() so those public methods never
panic on a zero-value Dynamic.
- Around line 305-308: The Reset() method currently unlocks d.mu before clearing
shared state d.Extracted, which can race with concurrent fetch/hydrate flows;
modify Reset() so that the assignment d.Extracted = nil is performed while
holding d.mu (i.e., move the d.Extracted = nil line between d.mu.Lock() and
d.mu.Unlock()) to ensure the mutex protects all shared-state modifications in
Reset() (referencing Reset(), d.mu, and d.Extracted).
- Around line 207-214: In fetchAndHydrate ensure you nil-check d.fetchCallback
before calling it: inside Dynamic.fetchAndHydrate (which currently locks d.mu
and calls d.fetchCallback(d)), if d.fetchCallback == nil set d.err to a
descriptive error (e.g. "nil fetchCallback") while holding the lock, call
d.resetOnce(), unlock and return; only call d.fetchCallback(d) when non-nil.
Make sure unlocking and reset behavior mirrors the existing error path.

---

Nitpick comments:
In `@pkg/authprovider/authx/dynamic_test.go`:
- Around line 1-3: TestEndToEndPrefetchFlow's comment says it verifies
concurrent GetStrategies() calls but the test only calls GetStrategies once;
either update the comment or add concurrent callers. Modify the
TestEndToEndPrefetchFlow test to spawn multiple goroutines that call
authx.GetStrategies() concurrently (use a sync.WaitGroup and channel to collect
errors/results) after calling authx.Fetch(), then assert all goroutines observe
the hydrated state; alternatively, if you prefer not to test concurrency, change
the test comment to remove "concurrent" and reflect the single-call behavior.
Ensure references to Fetch() and GetStrategies() remain in the test logic and
assertions.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 0092cab0-2aa6-445e-a3ad-cf27c1cc3af8

📥 Commits

Reviewing files that changed from the base of the PR and between 7bc65a2 and 855de19.

📒 Files selected for processing (2)
  • pkg/authprovider/authx/dynamic.go
  • pkg/authprovider/authx/dynamic_test.go

Comment on lines +42 to +45
func (d *Dynamic) getOnce() *sync.Once {
// Fast path - check if already initialized
ptr := d.once.Load()
if ptr != 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

Public methods can panic when Validate() was not called first

Line 44 and Line 301 dereference d.once without checking initialization. Fetch(), GetStrategies(), or Reset() on a zero-value Dynamic can panic.

Suggested fix
+func (d *Dynamic) ensureOnceGuard() {
+	if d.once == nil {
+		d.once = &atomic.Pointer[*sync.Once]{}
+		once := &sync.Once{}
+		d.once.Store(&once)
+	}
+}
+
 func (d *Dynamic) getOnce() *sync.Once {
+	d.ensureOnceGuard()
 	// Fast path - check if already initialized
 	ptr := d.once.Load()
 	if ptr != nil {
 		return *ptr
 	}
@@
 func (d *Dynamic) Reset() {
+	d.ensureOnceGuard()
 	once := &sync.Once{}
 	d.once.Store(&once)

Also applies to: 299-302

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

In `@pkg/authprovider/authx/dynamic.go` around lines 42 - 45, The methods can
dereference a nil d.once when Validate() hasn't been called; update getOnce() to
lazily initialize and store a new *sync.Once when d.once.Load() returns nil
(create new(sync.Once), store it atomically) and return it, and replace direct
dereferences of d.once in Fetch, GetStrategies, and Reset with calls to
getOnce() so those public methods never panic on a zero-value Dynamic.

- Fix Reset() data race: move d.Extracted = nil inside mutex lock
- Add nil check for fetchCallback in fetchAndHydrate()
- Add nil check for d.once in getOnce() when Validate() not called

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@hanzhcn
Copy link
Author

hanzhcn commented Mar 5, 2026

@coderabbitai resume

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

♻️ Duplicate comments (1)
pkg/authprovider/authx/dynamic.go (1)

45-47: ⚠️ Potential issue | 🟠 Major

Synchronize d.once container init and reuse it in Reset().

Line 45 initializes d.once without synchronization, so concurrent first access can create multiple atomic.Pointer containers and break single-guard semantics; Line 314 can also panic if Reset() is called before Validate().

🔧 Suggested fix
+func (d *Dynamic) ensureOnceContainer() *atomic.Pointer[*sync.Once] {
+	if d.once != nil {
+		return d.once
+	}
+	d.mu.Lock()
+	defer d.mu.Unlock()
+	if d.once == nil {
+		d.once = &atomic.Pointer[*sync.Once]{}
+	}
+	return d.once
+}
+
 func (d *Dynamic) getOnce() *sync.Once {
-	// Handle case where Validate() was not called
-	if d.once == nil {
-		d.once = &atomic.Pointer[*sync.Once]{}
-	}
+	onceContainer := d.ensureOnceContainer()
 	// Fast path - check if already initialized
-	ptr := d.once.Load()
+	ptr := onceContainer.Load()
 	if ptr != nil {
 		return *ptr
 	}
 	// Slow path - create new sync.Once
 	once := &sync.Once{}
 	// Try to store - if another goroutine beat us, use theirs
-	if !d.once.CompareAndSwap(nil, &once) {
+	if !onceContainer.CompareAndSwap(nil, &once) {
 		// Someone else stored, use their value
-		return *d.once.Load()
+		return *onceContainer.Load()
 	}
 	return once
 }
 
 func (d *Dynamic) resetOnce() {
 	// Atomically swap with a new sync.Once
+	onceContainer := d.ensureOnceContainer()
 	once := &sync.Once{}
-	d.once.Store(&once)
+	onceContainer.Store(&once)
 }
 
 func (d *Dynamic) Reset() {
-	once := &sync.Once{}
-	d.once.Store(&once)
+	d.resetOnce()
 	if d.fetched != nil {
 		d.fetched.Store(false)
 	}

Also applies to: 56-58, 313-315

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

In `@pkg/authprovider/authx/dynamic.go` around lines 45 - 47, d.once is lazily
created without synchronization which can race and cause multiple atomic.Pointer
containers or panics in Reset()/Validate(); fix by ensuring a single
atomic.Pointer[*sync.Once] container is created atomically and reused: add a
small helper (e.g., ensureOnceContainer) that uses atomic.CompareAndSwap (or a
package-level sync.Mutex) to install a new &atomic.Pointer[*sync.Once] only if
nil, and call that helper at the start of Validate(), Reset(), and any other
entry points; in Reset() first call the helper, then safe Store a new *sync.Once
into the existing atomic.Pointer container (don’t replace the container itself
without synchronization).
🧹 Nitpick comments (1)
pkg/authprovider/authx/dynamic_test.go (1)

183-184: Replace sleep-based coordination with explicit barriers.

These time.Sleep waits keep concurrency tests timing-sensitive. Prefer explicit channel/WaitGroup barriers so CI timing variance doesn’t affect outcomes.

Also applies to: 240-241, 315-316, 499-500

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

In `@pkg/authprovider/authx/dynamic_test.go` around lines 183 - 184, Replace the
timing-sensitive time.Sleep calls with explicit synchronization: remove the
time.Sleep(10 * time.Millisecond) calls and instead use channel or
sync.WaitGroup barriers to signal readiness/completion (e.g., send/close on
fetchDone or use a done/ready channel) so the goroutines under test wait
deterministically; update the test code around the fetch goroutine and any
waiter logic to block on the channel/WaitGroup and then close or signal
fetchDone when the operation is truly finished (apply same change to the other
occurrences referenced).
🤖 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/authprovider/authx/dynamic.go`:
- Line 259: The code unconditionally calls d.fetched.Store(true) which can panic
because d.fetched is only initialized in Validate(); update fetchAndHydrate() to
ensure d.fetched is non-nil before calling Store by allocating a new atomic.Bool
(or whatever concrete type is used) when nil, mirroring the nil-check and
initialization pattern used in Reset() and IsFetched(); this prevents
GetStrategies()/getOnce() from triggering a nil pointer panic when invoked prior
to Validate(). Ensure you reference and modify fetchAndHydrate(), leaving usages
in GetStrategies(), Validate(), Reset(), and IsFetched() unchanged except for
the added safe initialization of d.fetched.

---

Duplicate comments:
In `@pkg/authprovider/authx/dynamic.go`:
- Around line 45-47: d.once is lazily created without synchronization which can
race and cause multiple atomic.Pointer containers or panics in
Reset()/Validate(); fix by ensuring a single atomic.Pointer[*sync.Once]
container is created atomically and reused: add a small helper (e.g.,
ensureOnceContainer) that uses atomic.CompareAndSwap (or a package-level
sync.Mutex) to install a new &atomic.Pointer[*sync.Once] only if nil, and call
that helper at the start of Validate(), Reset(), and any other entry points; in
Reset() first call the helper, then safe Store a new *sync.Once into the
existing atomic.Pointer container (don’t replace the container itself without
synchronization).

---

Nitpick comments:
In `@pkg/authprovider/authx/dynamic_test.go`:
- Around line 183-184: Replace the timing-sensitive time.Sleep calls with
explicit synchronization: remove the time.Sleep(10 * time.Millisecond) calls and
instead use channel or sync.WaitGroup barriers to signal readiness/completion
(e.g., send/close on fetchDone or use a done/ready channel) so the goroutines
under test wait deterministically; update the test code around the fetch
goroutine and any waiter logic to block on the channel/WaitGroup and then close
or signal fetchDone when the operation is truly finished (apply same change to
the other occurrences referenced).

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: f61dd5bb-f6d3-4805-932e-ce427a44d4c7

📥 Commits

Reviewing files that changed from the base of the PR and between 855de19 and 1dc940e.

📒 Files selected for processing (2)
  • pkg/authprovider/authx/dynamic.go
  • pkg/authprovider/authx/dynamic_test.go

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Mar 5, 2026

✅ Actions performed

Reviews resumed.

Guard d.fetched.Store(true) with nil check to prevent panic when
GetStrategies() is called before Validate(). Matches the nil-check
pattern already used in Reset() and IsFetched().

Addresses CodeRabbit feedback on PR projectdiscovery#7110.
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 (2)
pkg/authprovider/authx/dynamic.go (2)

315-325: ⚠️ Potential issue | 🟡 Minor

Reset() can panic if Validate() was not called because d.once may be nil.

Line 317 calls d.once.Store(&once) without checking if d.once is nil. While getOnce() lazily initializes d.once, Reset() does not. Calling Reset() on an unvalidated Dynamic instance will cause a nil pointer panic.

🛡️ Proposed fix to add nil check
 func (d *Dynamic) Reset() {
 	once := &sync.Once{}
+	if d.once == nil {
+		d.once = &atomic.Pointer[*sync.Once]{}
+	}
 	d.once.Store(&once)
 	if d.fetched != nil {
 		d.fetched.Store(false)
 	}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pkg/authprovider/authx/dynamic.go` around lines 315 - 325, Reset() can panic
because it calls d.once.Store(...) when d.once may be nil; change Reset to guard
against nil d.once (or initialize it) before calling Store. Specifically, update
Reset() to either use the same lazy init used by getOnce() (initialize d.once if
nil) or check if d.once is nil and skip the Store; reference the Dynamic.Reset
method and the existing getOnce()/Validate() initialization path so the fix
mirrors how d.once is created elsewhere and prevents a nil pointer dereference.

43-61: ⚠️ Potential issue | 🟡 Minor

Potential race on d.once initialization when Validate() was not called.

At lines 45-47, if d.once == nil and multiple goroutines call getOnce() concurrently, they could each create and assign a new atomic.Pointer[*sync.Once]{}. While the subsequent CompareAndSwap at line 56 ensures only one sync.Once is stored, different goroutines might end up with different atomic.Pointer instances, causing them to operate on separate guards.

This is a defensive path for when Validate() wasn't called. Consider using sync.OnceValue or a package-level initialization to avoid this edge case, or document that Validate() must be called before concurrent access.

🛡️ Proposed fix using sync.Once for atomic.Pointer initialization
 func (d *Dynamic) getOnce() *sync.Once {
 	// Handle case where Validate() was not called
 	if d.once == nil {
-		d.once = &atomic.Pointer[*sync.Once]{}
+		// Note: This is not thread-safe if Validate() was not called
+		// and multiple goroutines call getOnce() concurrently.
+		// Callers should ensure Validate() is called before concurrent use.
+		d.once = &atomic.Pointer[*sync.Once]{}
 	}

Alternatively, add a sync.Once guard for initializing d.once:

var initOnce sync.Once
func (d *Dynamic) getOnce() *sync.Once {
    initOnce.Do(func() {
        if d.once == nil {
            d.once = &atomic.Pointer[*sync.Once]{}
        }
    })
    // ... rest of method
}

However, this would share state across all Dynamic instances. The simplest fix is to document that Validate() must be called before concurrent access.

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

In `@pkg/authprovider/authx/dynamic.go` around lines 43 - 61, getOnce has a race
when d.once is nil because multiple goroutines can allocate different
atomic.Pointer instances; fix by making the initialization of d.once
per-instance atomic: add a new field on Dynamic (e.g., initMu sync.Mutex) and in
getOnce wrap the nil-check/assignment of d.once with initMu.Lock()/Unlock(),
then proceed with the existing fast/slow path logic using d.once, leaving the
CompareAndSwap and sync.Once creation unchanged; this ensures only one
atomic.Pointer[*sync.Once] is installed per Dynamic instance without requiring
external Validate() calls.
🧹 Nitpick comments (2)
pkg/authprovider/authx/dynamic_test.go (1)

182-184: Minor: time.Sleep for goroutine synchronization may cause flaky tests on slow CI.

While the channel-based synchronization is good, the time.Sleep(10 * time.Millisecond) at lines 183, 240, 315, and 499 introduces timing dependencies. On heavily loaded CI systems, goroutines may not have started and blocked on GetStrategies() before the sleep completes, potentially causing intermittent test failures.

Consider using a barrier or additional synchronization to confirm all goroutines have entered GetStrategies() before proceeding. However, this is a minor concern since the tests appear to work reliably based on PR comments.

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

In `@pkg/authprovider/authx/dynamic_test.go` around lines 182 - 184, Replace the
fragile time.Sleep synchronization with an explicit barrier: have each test
goroutine signal when it has reached the GetStrategies() blocking point (e.g.,
by calling startedWG.Done() or sending on a started channel) and have the main
test wait for all goroutines to be ready (startedWG.Wait() or receiving N
signals) before closing fetchDone; update the test cases that reference
GetStrategies, fetchDone, and the goroutine spawn logic to use this
WaitGroup/channel-based readiness check instead of time.Sleep(10 *
time.Millisecond).
pkg/authprovider/authx/dynamic.go (1)

212-263: Holding mutex during potentially slow fetchCallback may cause contention.

The mutex d.mu is held from line 213 through the entire fetchCallback(d) execution (line 222) and hydration. If the fetch callback involves network I/O (as suggested by the "slow network request" simulation in tests), this could block concurrent Error() and GetStrategies() readers for extended periods.

Consider restructuring to only hold the mutex when reading/writing d.err, not during the callback itself:

♻️ Proposed restructuring
 func (d *Dynamic) fetchAndHydrate() {
+	// Check if fetchCallback is nil before calling
+	if d.fetchCallback == nil {
+		d.mu.Lock()
+		d.err = fmt.Errorf("fetchCallback is not set for dynamic secret")
+		d.mu.Unlock()
+		d.resetOnce()
+		return
+	}
+	
+	// Execute callback without holding the lock (may be slow I/O)
+	fetchErr := d.fetchCallback(d)
+	
 	d.mu.Lock()
-	// Check if fetchCallback is nil before calling
-	if d.fetchCallback == nil {
-		d.err = fmt.Errorf("fetchCallback is not set for dynamic secret")
-		d.mu.Unlock()
-		// Reset once to allow retry on next call
-		d.resetOnce()
-		return
-	}
-	d.err = d.fetchCallback(d)
+	d.err = fetchErr
 	if d.err != nil {

However, this would require careful analysis to ensure d.Extracted writes in the callback don't race with reads elsewhere. The current approach is correct, just potentially slower under contention.

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

In `@pkg/authprovider/authx/dynamic.go` around lines 212 - 263, fetchAndHydrate
currently holds d.mu across the slow fetchCallback(d) and hydration, causing
contention; change it to only hold the mutex for short critical sections: take
the lock to check fetchCallback != nil and set any pre-call state, then unlock
before calling d.fetchCallback(d) so network I/O doesn't block readers; after
the callback returns, re-lock and perform the same validation and error
assignment (check d.err, len(d.Extracted), call d.resetOnce() on failure), run
applyValuesToSecret under the lock for d.Secret and each entry in d.Secrets, and
finally set d.fetched.Store(true) after unlocking as currently done—ensure you
reference and update the same fields used now (d.fetchCallback, d.err,
d.Extracted, d.Secret, d.Secrets, d.resetOnce, d.fetched) so no race is
introduced when the callback mutates those fields.
🤖 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/authprovider/authx/dynamic.go`:
- Around line 315-325: Reset() can panic because it calls d.once.Store(...) when
d.once may be nil; change Reset to guard against nil d.once (or initialize it)
before calling Store. Specifically, update Reset() to either use the same lazy
init used by getOnce() (initialize d.once if nil) or check if d.once is nil and
skip the Store; reference the Dynamic.Reset method and the existing
getOnce()/Validate() initialization path so the fix mirrors how d.once is
created elsewhere and prevents a nil pointer dereference.
- Around line 43-61: getOnce has a race when d.once is nil because multiple
goroutines can allocate different atomic.Pointer instances; fix by making the
initialization of d.once per-instance atomic: add a new field on Dynamic (e.g.,
initMu sync.Mutex) and in getOnce wrap the nil-check/assignment of d.once with
initMu.Lock()/Unlock(), then proceed with the existing fast/slow path logic
using d.once, leaving the CompareAndSwap and sync.Once creation unchanged; this
ensures only one atomic.Pointer[*sync.Once] is installed per Dynamic instance
without requiring external Validate() calls.

---

Nitpick comments:
In `@pkg/authprovider/authx/dynamic_test.go`:
- Around line 182-184: Replace the fragile time.Sleep synchronization with an
explicit barrier: have each test goroutine signal when it has reached the
GetStrategies() blocking point (e.g., by calling startedWG.Done() or sending on
a started channel) and have the main test wait for all goroutines to be ready
(startedWG.Wait() or receiving N signals) before closing fetchDone; update the
test cases that reference GetStrategies, fetchDone, and the goroutine spawn
logic to use this WaitGroup/channel-based readiness check instead of
time.Sleep(10 * time.Millisecond).

In `@pkg/authprovider/authx/dynamic.go`:
- Around line 212-263: fetchAndHydrate currently holds d.mu across the slow
fetchCallback(d) and hydration, causing contention; change it to only hold the
mutex for short critical sections: take the lock to check fetchCallback != nil
and set any pre-call state, then unlock before calling d.fetchCallback(d) so
network I/O doesn't block readers; after the callback returns, re-lock and
perform the same validation and error assignment (check d.err, len(d.Extracted),
call d.resetOnce() on failure), run applyValuesToSecret under the lock for
d.Secret and each entry in d.Secrets, and finally set d.fetched.Store(true)
after unlocking as currently done—ensure you reference and update the same
fields used now (d.fetchCallback, d.err, d.Extracted, d.Secret, d.Secrets,
d.resetOnce, d.fetched) so no race is introduced when the callback mutates those
fields.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: e11c978b-cd95-45ca-87da-060d29eebc3d

📥 Commits

Reviewing files that changed from the base of the PR and between 855de19 and 20a05fd.

📒 Files selected for processing (2)
  • pkg/authprovider/authx/dynamic.go
  • pkg/authprovider/authx/dynamic_test.go

- Remove redundant `d.Secret.` prefix from embedded Secret field access
- Changed `d.Secret.Headers` to `d.Headers` (8 occurrences)
- Removed unnecessary `d.Secret != nil` check for embedded field
- All tests passing, staticcheck clean
Copy link
Member

@Mzack9999 Mzack9999 left a comment

Choose a reason for hiding this comment

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

Looks like there is a potential deadlock:

check-template.yaml
login-template.yaml
secret.yaml

with this server:

package main

import (
	"fmt"
	"net/http"
	"os"
	"time"
)

func main() {
	http.HandleFunc("/accounts/login/", func(w http.ResponseWriter, r *http.Request) {
		fmt.Fprintf(os.Stderr, ">>> %s %s\n", r.Method, r.URL.Path)
		time.Sleep(500 * time.Millisecond)
		w.Header().Set("Set-Cookie", "session=abc123; Path=/")
		w.WriteHeader(302)
	})

	http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
		cookie := r.Header.Get("Cookie")
		fmt.Fprintf(os.Stderr, ">>> %s %s  cookie=%q\n", r.Method, r.URL.Path, cookie)
		if cookie == "" {
			fmt.Fprintf(os.Stderr, "    *** UNAUTHENTICATED (bug #6592) ***\n")
		} else {
			fmt.Fprintf(os.Stderr, "    +++ AUTHENTICATED (fix working) +++\n")
		}
		w.WriteHeader(200)
		fmt.Fprint(w, "hello")
	})

	fmt.Println("Server listening on :18999")
	if err := http.ListenAndServe(":18999", nil); err != nil {
		fmt.Fprintln(os.Stderr, err)
		os.Exit(1)
	}
}

Nuclei hangs

go run . -t check-template.yaml -secret-file secret.yaml -u http://127.0.0.1:18999 -ni -duc -ps -debug

                     __     _
   ____  __  _______/ /__  (_)
  / __ \/ / / / ___/ / _ \/ /
 / / / / /_/ / /__/ /  __/ /
/_/ /_/\__,_/\___/_/\___/_/   v3.7.0

                projectdiscovery.io

[INF] Current nuclei version: v3.7.0 (unknown) - remove '-duc' flag to enable update checks
[INF] Current nuclei-templates version: v10.3.9 (unknown) - remove '-duc' flag to enable update checks
[WRN] Scan results upload to cloud is disabled.
[INF] New templates added in latest release: 182
[INF] Templates loaded for current scan: 1
[WRN] Loading 1 unsigned templates for scan. Use with caution.
[INF] Targets loaded for current scan: 1
[INF] Pre-fetching secrets from authprovider[s]

@Mzack9999
Copy link
Member

Thank you for your contribution @hanzhcn! Adding retry logic alongside the race fix shows forward-thinking about resilience.

However, combining a retry mechanism with the race condition fix increases the PR's scope and risk. We merged #6976 which focuses tightly on the race condition using sync.Once — the idiomatic Go primitive for one-time initialization — with comprehensive concurrency tests. Retry logic for auth fetches would be a great standalone enhancement if there's demand for it in a future issue.

Closing this as the issue has been resolved. Thank you for contributing to nuclei!

@Mzack9999 Mzack9999 closed this Mar 20, 2026
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.

[BUG] Authenticated scanning starts executing templates before the secret-file template finishes

4 participants