mirror of
https://github.com/projectdiscovery/nuclei.git
synced 2025-12-17 17:56:56 +00:00
resolve deadlock that occurs when dynamic auth templates trigger recursive auth requests during execution. RCA: 1. `GetStrategies()` calls `Fetch()` to retrieve auth creds. 2. `Fetch()` executes auth template via cb. 3. template exec triggers HTTP requests requiring auth. 4. recursive calls `GetStrategies()` → `Fetch()` cause deadlock on mutex. notable changes: * add `fetching` flag to `Dynamic` struct to track fetch-in-progress state. * modify `GetStrategies()` to return empty strategies if already fetching. * update `Fetch()` method with proper recursive call prevention. * use mutex-protected flag reads to ensure thread safety. * refactor `GetStrategies()` with local function for code reuse. this prevents infinite recursion during auth template execution while maintaining proper sync and err handling. fixes goroutine deadlocks in auth system when using dynamic secrets with templates that require auth. Signed-off-by: Dwi Siswanto <git@dw1.io>
263 lines
6.9 KiB
Go
263 lines
6.9 KiB
Go
package authx
|
|
|
|
import (
|
|
"fmt"
|
|
"strings"
|
|
"sync"
|
|
|
|
"github.com/projectdiscovery/gologger"
|
|
"github.com/projectdiscovery/nuclei/v3/pkg/protocols/common/replacer"
|
|
"github.com/projectdiscovery/nuclei/v3/pkg/utils/json"
|
|
"github.com/projectdiscovery/utils/errkit"
|
|
sliceutil "github.com/projectdiscovery/utils/slice"
|
|
)
|
|
|
|
type LazyFetchSecret func(d *Dynamic) error
|
|
|
|
var (
|
|
_ json.Unmarshaler = &Dynamic{}
|
|
)
|
|
|
|
// Dynamic is a struct for dynamic secret or credential
|
|
// these are high level secrets that take action to generate the actual secret
|
|
// ex: username and password are dynamic secrets, the actual secret is the token obtained
|
|
// after authenticating with the username and password
|
|
type Dynamic struct {
|
|
*Secret `yaml:",inline"` // this is a static secret that will be generated after the dynamic secret is resolved
|
|
Secrets []*Secret `yaml:"secrets"`
|
|
TemplatePath string `json:"template" yaml:"template"`
|
|
Variables []KV `json:"variables" yaml:"variables"`
|
|
Input string `json:"input" yaml:"input"` // (optional) target for the dynamic secret
|
|
Extracted map[string]interface{} `json:"-" yaml:"-"` // extracted values from the dynamic secret
|
|
fetchCallback LazyFetchSecret `json:"-" yaml:"-"`
|
|
m *sync.Mutex `json:"-" yaml:"-"` // mutex for lazy fetch
|
|
fetched bool `json:"-" yaml:"-"` // flag to check if the secret has been fetched
|
|
fetching bool `json:"-" yaml:"-"` // flag to check if we're currently fetching (prevents recursion)
|
|
error error `json:"-" yaml:"-"` // error if any
|
|
}
|
|
|
|
func (d *Dynamic) GetDomainAndDomainRegex() ([]string, []string) {
|
|
var domains []string
|
|
var domainRegex []string
|
|
for _, secret := range d.Secrets {
|
|
domains = append(domains, secret.Domains...)
|
|
domainRegex = append(domainRegex, secret.DomainsRegex...)
|
|
}
|
|
if d.Secret != nil {
|
|
domains = append(domains, d.Domains...)
|
|
domainRegex = append(domainRegex, d.DomainsRegex...)
|
|
}
|
|
uniqueDomains := sliceutil.Dedupe(domains)
|
|
uniqueDomainRegex := sliceutil.Dedupe(domainRegex)
|
|
return uniqueDomains, uniqueDomainRegex
|
|
}
|
|
|
|
func (d *Dynamic) UnmarshalJSON(data []byte) error {
|
|
if d == nil {
|
|
return errkit.New("cannot unmarshal into nil Dynamic struct")
|
|
}
|
|
|
|
// Use an alias type (auxiliary) to avoid a recursive call in this method.
|
|
type Alias Dynamic
|
|
|
|
// If d.Secret was nil, json.Unmarshal will allocate a new Secret object
|
|
// and populate it from the top level JSON fields.
|
|
if err := json.Unmarshal(data, (*Alias)(d)); err != nil {
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// Validate validates the dynamic secret
|
|
func (d *Dynamic) Validate() error {
|
|
d.m = &sync.Mutex{}
|
|
if d.TemplatePath == "" {
|
|
return errkit.New(" template-path is required for dynamic secret")
|
|
}
|
|
if len(d.Variables) == 0 {
|
|
return errkit.New("variables are required for dynamic secret")
|
|
}
|
|
|
|
if d.Secret != nil {
|
|
d.skipCookieParse = true // skip cookie parsing in dynamic secrets during validation
|
|
if err := d.Secret.Validate(); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
for _, secret := range d.Secrets {
|
|
secret.skipCookieParse = true
|
|
if err := secret.Validate(); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// SetLazyFetchCallback sets the lazy fetch callback for the dynamic secret
|
|
func (d *Dynamic) SetLazyFetchCallback(callback LazyFetchSecret) {
|
|
d.fetchCallback = func(d *Dynamic) error {
|
|
err := callback(d)
|
|
d.fetched = true
|
|
if err != nil {
|
|
d.error = err
|
|
return err
|
|
}
|
|
if len(d.Extracted) == 0 {
|
|
return fmt.Errorf("no extracted values found for dynamic secret")
|
|
}
|
|
|
|
if d.Secret != nil {
|
|
if err := d.applyValuesToSecret(d.Secret); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
for _, secret := range d.Secrets {
|
|
if err := d.applyValuesToSecret(secret); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
}
|
|
|
|
func (d *Dynamic) applyValuesToSecret(secret *Secret) error {
|
|
// evaluate headers
|
|
for i, header := range secret.Headers {
|
|
if strings.Contains(header.Value, "{{") {
|
|
header.Value = replacer.Replace(header.Value, d.Extracted)
|
|
}
|
|
if strings.Contains(header.Key, "{{") {
|
|
header.Key = replacer.Replace(header.Key, d.Extracted)
|
|
}
|
|
secret.Headers[i] = header
|
|
}
|
|
|
|
// evaluate cookies
|
|
for i, cookie := range secret.Cookies {
|
|
if strings.Contains(cookie.Value, "{{") {
|
|
cookie.Value = replacer.Replace(cookie.Value, d.Extracted)
|
|
}
|
|
if strings.Contains(cookie.Key, "{{") {
|
|
cookie.Key = replacer.Replace(cookie.Key, d.Extracted)
|
|
}
|
|
if strings.Contains(cookie.Raw, "{{") {
|
|
cookie.Raw = replacer.Replace(cookie.Raw, d.Extracted)
|
|
}
|
|
secret.Cookies[i] = cookie
|
|
}
|
|
|
|
// evaluate query params
|
|
for i, query := range secret.Params {
|
|
if strings.Contains(query.Value, "{{") {
|
|
query.Value = replacer.Replace(query.Value, d.Extracted)
|
|
}
|
|
if strings.Contains(query.Key, "{{") {
|
|
query.Key = replacer.Replace(query.Key, d.Extracted)
|
|
}
|
|
secret.Params[i] = query
|
|
}
|
|
|
|
// check username, password and token
|
|
if strings.Contains(secret.Username, "{{") {
|
|
secret.Username = replacer.Replace(secret.Username, d.Extracted)
|
|
}
|
|
if strings.Contains(secret.Password, "{{") {
|
|
secret.Password = replacer.Replace(secret.Password, d.Extracted)
|
|
}
|
|
if strings.Contains(secret.Token, "{{") {
|
|
secret.Token = replacer.Replace(secret.Token, d.Extracted)
|
|
}
|
|
|
|
// now attempt to parse the cookies
|
|
secret.skipCookieParse = false
|
|
for i, cookie := range secret.Cookies {
|
|
if cookie.Raw != "" {
|
|
if err := cookie.Parse(); err != nil {
|
|
return fmt.Errorf("[%s] invalid raw cookie in cookiesAuth: %s", d.TemplatePath, err)
|
|
}
|
|
secret.Cookies[i] = cookie
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// GetStrategy returns the auth strategies for the dynamic secret
|
|
func (d *Dynamic) GetStrategies() []AuthStrategy {
|
|
getStrategies := func() []AuthStrategy {
|
|
var strategies []AuthStrategy
|
|
if d.Secret != nil {
|
|
strategies = append(strategies, d.GetStrategy())
|
|
}
|
|
|
|
for _, secret := range d.Secrets {
|
|
strategies = append(strategies, secret.GetStrategy())
|
|
}
|
|
|
|
return strategies
|
|
}
|
|
|
|
d.m.Lock()
|
|
isFetched := d.fetched
|
|
isFetching := d.fetching
|
|
d.m.Unlock()
|
|
|
|
if isFetched {
|
|
if d.error != nil {
|
|
return nil
|
|
}
|
|
|
|
return getStrategies()
|
|
}
|
|
|
|
if isFetching {
|
|
// NOTE(dwisiswant0): Bail out w/ empty here, or we will yeet into
|
|
// recursion hell. See line 242-245.
|
|
return nil
|
|
}
|
|
|
|
_ = d.Fetch(true)
|
|
if d.error != nil {
|
|
return nil
|
|
}
|
|
|
|
return getStrategies()
|
|
}
|
|
|
|
// Fetch fetches the dynamic secret
|
|
// if isFatal is true, it will stop the execution if the secret could not be fetched
|
|
func (d *Dynamic) Fetch(isFatal bool) error {
|
|
d.m.Lock()
|
|
defer d.m.Unlock()
|
|
|
|
if d.fetched {
|
|
return d.error
|
|
}
|
|
|
|
if d.fetching {
|
|
return nil
|
|
}
|
|
|
|
d.fetching = true
|
|
defer func() {
|
|
d.fetching = false
|
|
}()
|
|
|
|
d.error = d.fetchCallback(d)
|
|
if d.error != nil {
|
|
if isFatal {
|
|
gologger.Fatal().Msgf("Could not fetch dynamic secret: %s\n", d.error)
|
|
}
|
|
} else {
|
|
d.fetched = true
|
|
}
|
|
|
|
return d.error
|
|
}
|
|
|
|
// Error returns the error if any
|
|
func (d *Dynamic) Error() error {
|
|
return d.error
|
|
}
|