Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 31 additions & 0 deletions common/authprovider/authx/basic_auth.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package authx

import (
"net/http"

"github.com/projectdiscovery/retryablehttp-go"
)

var (
_ AuthStrategy = &BasicAuthStrategy{}
)

// BasicAuthStrategy is a strategy for basic auth
type BasicAuthStrategy struct {
Data *Secret
}

// NewBasicAuthStrategy creates a new basic auth strategy
func NewBasicAuthStrategy(data *Secret) *BasicAuthStrategy {
return &BasicAuthStrategy{Data: data}
}

// Apply applies the basic auth strategy to the request
func (s *BasicAuthStrategy) Apply(req *http.Request) {
req.SetBasicAuth(s.Data.Username, s.Data.Password)
}

// ApplyOnRR applies the basic auth strategy to the retryable request
func (s *BasicAuthStrategy) ApplyOnRR(req *retryablehttp.Request) {
req.SetBasicAuth(s.Data.Username, s.Data.Password)
}
31 changes: 31 additions & 0 deletions common/authprovider/authx/bearer_auth.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package authx

import (
"net/http"

"github.com/projectdiscovery/retryablehttp-go"
)

var (
_ AuthStrategy = &BearerTokenAuthStrategy{}
)

// BearerTokenAuthStrategy is a strategy for bearer token auth
type BearerTokenAuthStrategy struct {
Data *Secret
}

// NewBearerTokenAuthStrategy creates a new bearer token auth strategy
func NewBearerTokenAuthStrategy(data *Secret) *BearerTokenAuthStrategy {
return &BearerTokenAuthStrategy{Data: data}
}

// Apply applies the bearer token auth strategy to the request
func (s *BearerTokenAuthStrategy) Apply(req *http.Request) {
req.Header.Set("Authorization", "Bearer "+s.Data.Token)
}

// ApplyOnRR applies the bearer token auth strategy to the retryable request
func (s *BearerTokenAuthStrategy) ApplyOnRR(req *retryablehttp.Request) {
req.Header.Set("Authorization", "Bearer "+s.Data.Token)
}
62 changes: 62 additions & 0 deletions common/authprovider/authx/cookies_auth.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
package authx

import (
"net/http"

"github.com/projectdiscovery/retryablehttp-go"
)

var (
_ AuthStrategy = &CookiesAuthStrategy{}
)

// CookiesAuthStrategy is a strategy for cookies auth
type CookiesAuthStrategy struct {
Data *Secret
}

// NewCookiesAuthStrategy creates a new cookies auth strategy
func NewCookiesAuthStrategy(data *Secret) *CookiesAuthStrategy {
return &CookiesAuthStrategy{Data: data}
}

// Apply applies the cookies auth strategy to the request
func (s *CookiesAuthStrategy) Apply(req *http.Request) {
for _, cookie := range s.Data.Cookies {
req.AddCookie(&http.Cookie{
Name: cookie.Key,
Value: cookie.Value,
})
}
}

// ApplyOnRR applies the cookies auth strategy to the retryable request
func (s *CookiesAuthStrategy) ApplyOnRR(req *retryablehttp.Request) {
// Build a set of cookie names to replace
newCookieNames := make(map[string]struct{}, len(s.Data.Cookies))
for _, cookie := range s.Data.Cookies {
newCookieNames[cookie.Key] = struct{}{}
}

// Filter existing cookies, keeping only those not being replaced
existingCookies := req.Cookies()
filteredCookies := make([]*http.Cookie, 0, len(existingCookies))
for _, cookie := range existingCookies {
if _, shouldReplace := newCookieNames[cookie.Name]; !shouldReplace {
filteredCookies = append(filteredCookies, cookie)
}
}

// Clear and reset cookies
req.Header.Del("Cookie")
for _, cookie := range filteredCookies {
req.AddCookie(cookie)
}
// Add new cookies
for _, cookie := range s.Data.Cookies {
req.AddCookie(&http.Cookie{
Name: cookie.Key,
Value: cookie.Value,
})
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}
244 changes: 244 additions & 0 deletions common/authprovider/authx/file.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,244 @@
package authx

import (
"encoding/json"
"fmt"
"os"
"path/filepath"
"regexp"
"strings"

"github.com/projectdiscovery/utils/errkit"
"github.com/projectdiscovery/utils/generic"
stringsutil "github.com/projectdiscovery/utils/strings"
"gopkg.in/yaml.v3"
)

type AuthType string

const (
BasicAuth AuthType = "BasicAuth"
BearerTokenAuth AuthType = "BearerToken"
HeadersAuth AuthType = "Header"
CookiesAuth AuthType = "Cookie"
QueryAuth AuthType = "Query"
)

// SupportedAuthTypes returns the supported auth types
func SupportedAuthTypes() []string {
return []string{
string(BasicAuth),
string(BearerTokenAuth),
string(HeadersAuth),
string(CookiesAuth),
string(QueryAuth),
}
}

// Authx is a struct for secrets or credentials file
type Authx struct {
ID string `json:"id" yaml:"id"`
Info AuthFileInfo `json:"info" yaml:"info"`
Secrets []Secret `json:"static" yaml:"static"`
}

type AuthFileInfo struct {
Name string `json:"name" yaml:"name"`
Author string `json:"author" yaml:"author"`
Severity string `json:"severity" yaml:"severity"`
Description string `json:"description" yaml:"description"`
}

// Secret is a struct for secret or credential
type Secret struct {
Type string `json:"type" yaml:"type"`
Domains []string `json:"domains" yaml:"domains"`
DomainsRegex []string `json:"domains-regex" yaml:"domains-regex"`
Headers []KV `json:"headers" yaml:"headers"` // Headers preserve exact casing (useful for case-sensitive APIs)
Cookies []Cookie `json:"cookies" yaml:"cookies"`
Params []KV `json:"params" yaml:"params"`
Username string `json:"username" yaml:"username"` // can be either email or username
Password string `json:"password" yaml:"password"`
Token string `json:"token" yaml:"token"` // Bearer Auth token
}

// GetStrategy returns the auth strategy for the secret
func (s *Secret) GetStrategy() AuthStrategy {
switch {
case strings.EqualFold(s.Type, string(BasicAuth)):
return NewBasicAuthStrategy(s)
case strings.EqualFold(s.Type, string(BearerTokenAuth)):
return NewBearerTokenAuthStrategy(s)
case strings.EqualFold(s.Type, string(HeadersAuth)):
return NewHeadersAuthStrategy(s)
case strings.EqualFold(s.Type, string(CookiesAuth)):
return NewCookiesAuthStrategy(s)
case strings.EqualFold(s.Type, string(QueryAuth)):
return NewQueryAuthStrategy(s)
}
return nil
}

func (s *Secret) Validate() error {
if !stringsutil.EqualFoldAny(s.Type, SupportedAuthTypes()...) {
return fmt.Errorf("invalid type: %s", s.Type)
}
if len(s.Domains) == 0 && len(s.DomainsRegex) == 0 {
return fmt.Errorf("domains or domains-regex cannot be empty")
}
if len(s.DomainsRegex) > 0 {
for _, domain := range s.DomainsRegex {
_, err := regexp.Compile(domain)
if err != nil {
return fmt.Errorf("invalid domain regex: %s", domain)
}
}
}

switch {
case strings.EqualFold(s.Type, string(BasicAuth)):
if s.Username == "" {
return fmt.Errorf("username cannot be empty in basic auth")
}
if s.Password == "" {
return fmt.Errorf("password cannot be empty in basic auth")
}
case strings.EqualFold(s.Type, string(BearerTokenAuth)):
if s.Token == "" {
return fmt.Errorf("token cannot be empty in bearer token auth")
}
case strings.EqualFold(s.Type, string(HeadersAuth)):
if len(s.Headers) == 0 {
return fmt.Errorf("headers cannot be empty in headers auth")
}
for _, header := range s.Headers {
if err := header.Validate(); err != nil {
return fmt.Errorf("invalid header in headersAuth: %s", err)
}
}
case strings.EqualFold(s.Type, string(CookiesAuth)):
if len(s.Cookies) == 0 {
return fmt.Errorf("cookies cannot be empty in cookies auth")
}
for _, cookie := range s.Cookies {
if cookie.Raw != "" {
if err := cookie.Parse(); err != nil {
return fmt.Errorf("invalid raw cookie in cookiesAuth: %s", err)
}
}
if err := cookie.Validate(); err != nil {
return fmt.Errorf("invalid cookie in cookiesAuth: %s", err)
}
}
Comment on lines +119 to +132
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Bug: Raw cookie parsing doesn't persist to original slice.

The loop iterates over s.Cookies by value, so cookie is a copy. When cookie.Parse() populates Key and Value, only the local copy is modified—the original Cookie in s.Cookies remains unchanged with empty Key/Value fields. This means raw cookies won't work when the strategy later tries to apply them.

Proposed fix - iterate by index
 	case strings.EqualFold(s.Type, string(CookiesAuth)):
 		if len(s.Cookies) == 0 {
 			return fmt.Errorf("cookies cannot be empty in cookies auth")
 		}
-		for _, cookie := range s.Cookies {
-			if cookie.Raw != "" {
-				if err := cookie.Parse(); err != nil {
+		for i := range s.Cookies {
+			if s.Cookies[i].Raw != "" {
+				if err := s.Cookies[i].Parse(); err != nil {
 					return fmt.Errorf("invalid raw cookie in cookiesAuth: %s", err)
 				}
 			}
-			if err := cookie.Validate(); err != nil {
+			if err := s.Cookies[i].Validate(); err != nil {
 				return fmt.Errorf("invalid cookie in cookiesAuth: %s", err)
 			}
 		}
📝 Committable suggestion

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

Suggested change
case strings.EqualFold(s.Type, string(CookiesAuth)):
if len(s.Cookies) == 0 {
return fmt.Errorf("cookies cannot be empty in cookies auth")
}
for _, cookie := range s.Cookies {
if cookie.Raw != "" {
if err := cookie.Parse(); err != nil {
return fmt.Errorf("invalid raw cookie in cookiesAuth: %s", err)
}
}
if err := cookie.Validate(); err != nil {
return fmt.Errorf("invalid cookie in cookiesAuth: %s", err)
}
}
case strings.EqualFold(s.Type, string(CookiesAuth)):
if len(s.Cookies) == 0 {
return fmt.Errorf("cookies cannot be empty in cookies auth")
}
for i := range s.Cookies {
if s.Cookies[i].Raw != "" {
if err := s.Cookies[i].Parse(); err != nil {
return fmt.Errorf("invalid raw cookie in cookiesAuth: %s", err)
}
}
if err := s.Cookies[i].Validate(); err != nil {
return fmt.Errorf("invalid cookie in cookiesAuth: %s", err)
}
}
🤖 Prompt for AI Agents
In @common/authprovider/authx/file.go around lines 119 - 132, The validation
loop for CookiesAuth iterates s.Cookies by value so cookie.Parse() updates only
the copy; change the loop to iterate by index (or range over pointers) and
operate on &s.Cookies[i] (or s.Cookies[i].Parse/Validate) so parsed Key/Value
are persisted back into the original slice; ensure you still call Validate() on
the modified element and return the same formatted errors from the CookiesAuth
branch.

case strings.EqualFold(s.Type, string(QueryAuth)):
if len(s.Params) == 0 {
return fmt.Errorf("query cannot be empty in query auth")
}
for _, query := range s.Params {
if err := query.Validate(); err != nil {
return fmt.Errorf("invalid query in queryAuth: %s", err)
}
}
}
return nil
}

type KV struct {
Key string `json:"key" yaml:"key"` // Header key (preserves exact casing)
Value string `json:"value" yaml:"value"`
}

func (k *KV) Validate() error {
if k.Key == "" {
return fmt.Errorf("key cannot be empty")
}
if k.Value == "" {
return fmt.Errorf("value cannot be empty")
}
return nil
}

type Cookie struct {
Key string `json:"key" yaml:"key"`
Value string `json:"value" yaml:"value"`
Raw string `json:"raw" yaml:"raw"`
}

func (c *Cookie) Validate() error {
if c.Raw != "" {
return nil
}
if c.Key == "" {
return fmt.Errorf("key cannot be empty")
}
if c.Value == "" {
return fmt.Errorf("value cannot be empty")
}
return nil
}

// Parse parses the cookie
// in raw the cookie is in format of
// Set-Cookie: <cookie-name>=<cookie-value>; Expires=<date>; Path=<path>; Domain=<domain_name>; Secure; HttpOnly
func (c *Cookie) Parse() error {
if c.Raw == "" {
return fmt.Errorf("raw cookie cannot be empty")
}
tmp := strings.TrimPrefix(c.Raw, "Set-Cookie: ")
slice := strings.Split(tmp, ";")
if len(slice) == 0 {
return fmt.Errorf("invalid raw cookie no ; found")
}
// first element is the cookie name and value
// Use SplitN to preserve '=' characters in the cookie value
cookie := strings.SplitN(slice[0], "=", 2)
if len(cookie) != 2 {
return fmt.Errorf("invalid raw cookie: missing '=' in cookie name=value: %s", c.Raw)
}
c.Key = strings.TrimSpace(cookie[0])
c.Value = strings.TrimSpace(cookie[1])
if c.Key == "" {
return fmt.Errorf("invalid raw cookie: empty cookie name: %s", c.Raw)
}
return nil
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

// GetAuthDataFromFile reads the auth data from file
func GetAuthDataFromFile(file string) (*Authx, error) {
ext := strings.ToLower(filepath.Ext(file))
if !generic.EqualsAny(ext, ".yml", ".yaml", ".json") {
return nil, fmt.Errorf("invalid file extension: supported extensions are .yml,.yaml and .json got %s", ext)
}
bin, err := os.ReadFile(file)
if err != nil {
return nil, err
}
if ext == ".yml" || ext == ".yaml" {
return GetAuthDataFromYAML(bin)
}
return GetAuthDataFromJSON(bin)
}

// GetAuthDataFromYAML reads the auth data from yaml
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
}
return &auth, nil
}

// GetAuthDataFromJSON reads the auth data from json
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
}
return &auth, nil
}
Loading
Loading