mirror of
https://github.com/projectdiscovery/nuclei.git
synced 2025-12-17 13:55:24 +00:00
* Refactored header-based auth scans not to normalize the header names. * Removed the header validation as it's not really useful here. * adding docs --------- Co-authored-by: Mzack9999 <mzack9999@protonmail.com>
258 lines
7.3 KiB
Go
258 lines
7.3 KiB
Go
package authx
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"regexp"
|
|
"strings"
|
|
|
|
"github.com/projectdiscovery/nuclei/v3/pkg/utils/json"
|
|
"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"`
|
|
Dynamic []Dynamic `json:"dynamic" yaml:"dynamic"`
|
|
}
|
|
|
|
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
|
|
skipCookieParse bool `json:"-" yaml:"-"` // temporary flag to skip cookie parsing (used in dynamic secrets)
|
|
}
|
|
|
|
// 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 != "" && !s.skipCookieParse {
|
|
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(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)
|
|
}
|
|
}
|
|
default:
|
|
return fmt.Errorf("invalid type: %s", s.Type)
|
|
}
|
|
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
|
|
cookie := strings.Split(slice[0], "=")
|
|
if len(cookie) == 2 {
|
|
c.Key = cookie[0]
|
|
c.Value = cookie[1]
|
|
return nil
|
|
}
|
|
return fmt.Errorf("invalid raw cookie: %s", c.Raw)
|
|
}
|
|
|
|
// GetAuthDataFromFile reads the auth data from file
|
|
func GetAuthDataFromFile(file string) (*Authx, error) {
|
|
ext := 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)
|
|
}
|
|
|
|
// GetTemplatePathsFromSecretFile reads the template IDs from the secret file
|
|
func GetTemplatePathsFromSecretFile(file string) ([]string, error) {
|
|
auth, err := GetAuthDataFromFile(file)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
var paths []string
|
|
for _, dynamic := range auth.Dynamic {
|
|
paths = append(paths, dynamic.TemplatePath)
|
|
}
|
|
return paths, nil
|
|
}
|
|
|
|
// 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
|
|
}
|