2024-03-14 03:08:53 +05:30
package openapi
import (
"bytes"
"fmt"
"io"
"mime/multipart"
"net/http"
"net/http/httputil"
"net/url"
"os"
"strings"
"github.com/clbanning/mxj/v2"
"github.com/getkin/kin-openapi/openapi3"
"github.com/pkg/errors"
"github.com/projectdiscovery/gologger"
"github.com/projectdiscovery/nuclei/v3/pkg/catalog/config"
"github.com/projectdiscovery/nuclei/v3/pkg/input/formats"
httpTypes "github.com/projectdiscovery/nuclei/v3/pkg/input/types"
"github.com/projectdiscovery/nuclei/v3/pkg/types"
2025-02-11 04:31:37 +07:00
"github.com/projectdiscovery/nuclei/v3/pkg/utils/json"
2025-08-20 05:28:23 +05:30
"github.com/projectdiscovery/utils/errkit"
2024-03-14 03:08:53 +05:30
"github.com/projectdiscovery/utils/generic"
mapsutil "github.com/projectdiscovery/utils/maps"
"github.com/valyala/fasttemplate"
)
const (
2024-09-19 19:29:19 +05:30
globalAuth = "globalAuth"
DEFAULT_HTTP_SCHEME_HEADER = "Authorization"
2024-03-14 03:08:53 +05:30
)
// GenerateRequestsFromSchema generates http requests from an OpenAPI 3.0 document object
func GenerateRequestsFromSchema ( schema * openapi3 . T , opts formats . InputFormatOptions , callback formats . ParseReqRespCallback ) error {
if len ( schema . Servers ) == 0 {
return errors . New ( "no servers found in openapi schema" )
}
// new set of globalParams obtained from security schemes
globalParams := openapi3 . NewParameters ( )
if len ( schema . Security ) > 0 {
params , err := GetGlobalParamsForSecurityRequirement ( schema , & schema . Security )
if err != nil {
return err
}
globalParams = append ( globalParams , params ... )
}
// validate global param requirements
for _ , param := range globalParams {
if val , ok := opts . Variables [ param . Value . Name ] ; ok {
param . Value . Example = val
} else {
// if missing check for validation
if opts . SkipFormatValidation {
gologger . Verbose ( ) . Msgf ( "openapi: skipping all requests due to missing global auth parameter: %s\n" , param . Value . Name )
return nil
} else {
// fatal error
gologger . Fatal ( ) . Msgf ( "openapi: missing global auth parameter: %s\n" , param . Value . Name )
}
}
}
missingVarMap := make ( map [ string ] struct { } )
optionalVarMap := make ( map [ string ] struct { } )
missingParamValueCallback := func ( param * openapi3 . Parameter , opts * generateReqOptions ) {
if ! param . Required {
optionalVarMap [ param . Name ] = struct { } { }
return
}
missingVarMap [ param . Name ] = struct { } { }
}
2024-08-16 08:01:23 -07:00
2024-03-14 03:08:53 +05:30
for _ , serverURL := range schema . Servers {
pathURL := serverURL . URL
2024-08-16 15:07:02 +03:00
// Split the server URL into baseURL and serverPath
u , err := url . Parse ( pathURL )
if err != nil {
return errors . Wrap ( err , "could not parse server url" )
}
baseURL := fmt . Sprintf ( "%s://%s" , u . Scheme , u . Host )
serverPath := u . Path
2024-03-14 03:08:53 +05:30
for path , v := range schema . Paths . Map ( ) {
// a path item can have parameters
ops := v . Operations ( )
requestPath := path
2024-08-16 15:07:02 +03:00
if serverPath != "" {
requestPath = serverPath + path
}
2024-03-14 03:08:53 +05:30
for method , ov := range ops {
if err := generateRequestsFromOp ( & generateReqOptions {
requiredOnly : opts . RequiredOnly ,
method : method ,
2024-08-16 15:07:02 +03:00
pathURL : baseURL ,
2024-03-14 03:08:53 +05:30
requestPath : requestPath ,
op : ov ,
schema : schema ,
globalParams : globalParams ,
reqParams : v . Parameters ,
opts : opts ,
callback : callback ,
missingParamValueCallback : missingParamValueCallback ,
} ) ; err != nil {
gologger . Warning ( ) . Msgf ( "Could not generate requests from op: %s\n" , err )
}
}
}
}
if len ( missingVarMap ) > 0 && ! opts . SkipFormatValidation {
gologger . Error ( ) . Msgf ( "openapi: Found %d missing parameters, use -skip-format-validation flag to skip requests or update missing parameters generated in %s file,you can also specify these vars using -var flag in (key=value) format\n" , len ( missingVarMap ) , formats . DefaultVarDumpFileName )
gologger . Verbose ( ) . Msgf ( "openapi: missing params: %+v" , mapsutil . GetSortedKeys ( missingVarMap ) )
if config . CurrentAppMode == config . AppModeCLI {
// generate var dump file
vars := & formats . OpenAPIParamsCfgFile { }
for k := range missingVarMap {
vars . Var = append ( vars . Var , k + "=" )
}
vars . OptionalVars = mapsutil . GetSortedKeys ( optionalVarMap )
if err := formats . WriteOpenAPIVarDumpFile ( vars ) ; err != nil {
gologger . Error ( ) . Msgf ( "openapi: could not write params file: %s\n" , err )
}
// exit with status code 1
os . Exit ( 1 )
}
}
return nil
}
type generateReqOptions struct {
// requiredOnly specifies whether to generate only required fields
requiredOnly bool
// method is the http method to use
method string
// pathURL is the base url to use
pathURL string
// requestPath is the path to use
requestPath string
// schema is the openapi schema to use
schema * openapi3 . T
// op is the operation to use
op * openapi3 . Operation
// post request generation callback
callback formats . ParseReqRespCallback
// global parameters
globalParams openapi3 . Parameters
// requestparams map
reqParams openapi3 . Parameters
// global var map
opts formats . InputFormatOptions
// missingVar Callback
missingParamValueCallback func ( param * openapi3 . Parameter , opts * generateReqOptions )
}
// generateRequestsFromOp generates requests from an operation and some other data
// about an OpenAPI Schema Path and Method object.
//
// It also accepts an optional requiredOnly flag which if specified, only returns the fields
// of the structure that are required. If false, all fields are returned.
func generateRequestsFromOp ( opts * generateReqOptions ) error {
req , err := http . NewRequest ( opts . method , opts . pathURL + opts . requestPath , nil )
if err != nil {
return errors . Wrap ( err , "could not make request" )
}
reqParams := opts . reqParams
if reqParams == nil {
reqParams = openapi3 . NewParameters ( )
}
// add existing req params
reqParams = append ( reqParams , opts . op . Parameters ... )
// check for endpoint specific auth
if opts . op . Security != nil {
params , err := GetGlobalParamsForSecurityRequirement ( opts . schema , opts . op . Security )
if err != nil {
return err
}
reqParams = append ( reqParams , params ... )
} else {
reqParams = append ( reqParams , opts . globalParams ... )
}
query := url . Values { }
for _ , parameter := range reqParams {
value := parameter . Value
2024-04-23 14:26:33 +03:00
if value . Schema == nil || value . Schema . Value == nil {
continue
}
2024-03-14 03:08:53 +05:30
// paramValue or default value to use
var paramValue interface { }
// accept override from global variables
if val , ok := opts . opts . Variables [ value . Name ] ; ok {
paramValue = val
} else if value . Schema . Value . Default != nil {
paramValue = value . Schema . Value . Default
} else if value . Schema . Value . Example != nil {
paramValue = value . Schema . Value . Example
2024-08-16 08:01:23 -07:00
} else if len ( value . Schema . Value . Enum ) > 0 {
2024-03-14 03:08:53 +05:30
paramValue = value . Schema . Value . Enum [ 0 ]
} else {
if ! opts . opts . SkipFormatValidation {
if opts . missingParamValueCallback != nil {
opts . missingParamValueCallback ( value , opts )
}
// skip request if param in path else skip this param only
if value . Required {
// gologger.Verbose().Msgf("skipping request [%s] %s due to missing value (%v)\n", opts.method, opts.requestPath, value.Name)
return nil
} else {
// if it is in path then remove it from path
2025-07-01 00:40:44 +07:00
opts . requestPath = strings . ReplaceAll ( opts . requestPath , fmt . Sprintf ( "{%s}" , value . Name ) , "" )
2024-03-14 03:08:53 +05:30
if ! opts . opts . RequiredOnly {
gologger . Verbose ( ) . Msgf ( "openapi: skipping optional param (%s) in (%v) in request [%s] %s due to missing value (%v)\n" , value . Name , value . In , opts . method , opts . requestPath , value . Name )
}
continue
}
}
exampleX , err := generateExampleFromSchema ( value . Schema . Value )
if err != nil {
// when failed to generate example
// skip request if param in path else skip this param only
if value . Required {
gologger . Verbose ( ) . Msgf ( "openapi: skipping request [%s] %s due to missing value (%v)\n" , opts . method , opts . requestPath , value . Name )
return nil
} else {
// if it is in path then remove it from path
2025-07-01 00:40:44 +07:00
opts . requestPath = strings . ReplaceAll ( opts . requestPath , fmt . Sprintf ( "{%s}" , value . Name ) , "" )
2024-03-14 03:08:53 +05:30
if ! opts . opts . RequiredOnly {
2025-02-04 09:14:52 +01:00
gologger . Verbose ( ) . Msgf ( "openapi: skipping optional param (%s) in (%v) in request [%s] %s due to missing value (%v)\n" , value . Name , value . In , opts . method , opts . requestPath , value . Name )
2024-03-14 03:08:53 +05:30
}
continue
}
}
paramValue = exampleX
}
if opts . requiredOnly && ! value . Required {
// remove them from path if any
2025-07-01 00:40:44 +07:00
opts . requestPath = strings . ReplaceAll ( opts . requestPath , fmt . Sprintf ( "{%s}" , value . Name ) , "" )
2024-03-14 03:08:53 +05:30
continue // Skip this parameter if it is not required and we want only required ones
}
switch value . In {
case "query" :
query . Set ( value . Name , types . ToString ( paramValue ) )
case "header" :
req . Header . Set ( value . Name , types . ToString ( paramValue ) )
case "path" :
opts . requestPath = fasttemplate . ExecuteStringStd ( opts . requestPath , "{" , "}" , map [ string ] interface { } {
value . Name : types . ToString ( paramValue ) ,
} )
case "cookie" :
req . AddCookie ( & http . Cookie { Name : value . Name , Value : types . ToString ( paramValue ) } )
}
}
req . URL . RawQuery = query . Encode ( )
req . URL . Path = opts . requestPath
if opts . op . RequestBody != nil {
for content , value := range opts . op . RequestBody . Value . Content {
cloned := req . Clone ( req . Context ( ) )
2025-05-17 02:16:41 +07:00
var val interface { }
if value . Schema == nil || value . Schema . Value == nil {
val = generateEmptySchemaValue ( content )
} else {
var err error
val , err = generateExampleFromSchema ( value . Schema . Value )
if err != nil {
continue
}
2024-03-14 03:08:53 +05:30
}
// var body string
switch content {
case "application/json" :
2025-05-17 02:16:41 +07:00
if marshalled , err := json . Marshal ( val ) ; err == nil {
2024-03-14 03:08:53 +05:30
// body = string(marshalled)
cloned . Body = io . NopCloser ( bytes . NewReader ( marshalled ) )
cloned . ContentLength = int64 ( len ( marshalled ) )
cloned . Header . Set ( "Content-Type" , "application/json" )
}
case "application/xml" :
2025-05-17 02:16:41 +07:00
values := mxj . Map ( val . ( map [ string ] interface { } ) )
2024-03-14 03:08:53 +05:30
2025-05-17 02:16:41 +07:00
if marshalled , err := values . Xml ( ) ; err == nil {
2024-03-14 03:08:53 +05:30
// body = string(marshalled)
cloned . Body = io . NopCloser ( bytes . NewReader ( marshalled ) )
cloned . ContentLength = int64 ( len ( marshalled ) )
cloned . Header . Set ( "Content-Type" , "application/xml" )
} else {
gologger . Warning ( ) . Msgf ( "openapi: could not encode xml" )
}
case "application/x-www-form-urlencoded" :
2025-05-17 02:16:41 +07:00
if values , ok := val . ( map [ string ] interface { } ) ; ok {
2024-03-14 03:08:53 +05:30
cloned . Form = url . Values { }
for k , v := range values {
cloned . Form . Set ( k , types . ToString ( v ) )
}
encoded := cloned . Form . Encode ( )
cloned . ContentLength = int64 ( len ( encoded ) )
// body = encoded
cloned . Body = io . NopCloser ( strings . NewReader ( encoded ) )
cloned . Header . Set ( "Content-Type" , "application/x-www-form-urlencoded" )
}
case "multipart/form-data" :
2025-05-17 02:16:41 +07:00
if values , ok := val . ( map [ string ] interface { } ) ; ok {
2024-03-14 03:08:53 +05:30
buffer := & bytes . Buffer { }
multipartWriter := multipart . NewWriter ( buffer )
for k , v := range values {
// This is a file if format is binary, otherwise field
if property , ok := value . Schema . Value . Properties [ k ] ; ok && property . Value . Format == "binary" {
if writer , err := multipartWriter . CreateFormFile ( k , k ) ; err == nil {
_ , _ = writer . Write ( [ ] byte ( types . ToString ( v ) ) )
}
} else {
_ = multipartWriter . WriteField ( k , types . ToString ( v ) )
}
}
2025-07-01 00:40:44 +07:00
_ = multipartWriter . Close ( )
2024-03-14 03:08:53 +05:30
// body = buffer.String()
cloned . Body = io . NopCloser ( buffer )
cloned . ContentLength = int64 ( len ( buffer . Bytes ( ) ) )
cloned . Header . Set ( "Content-Type" , multipartWriter . FormDataContentType ( ) )
}
case "text/plain" :
2025-05-17 02:16:41 +07:00
str := types . ToString ( val )
2024-03-14 03:08:53 +05:30
// body = str
cloned . Body = io . NopCloser ( strings . NewReader ( str ) )
cloned . ContentLength = int64 ( len ( str ) )
cloned . Header . Set ( "Content-Type" , "text/plain" )
case "application/octet-stream" :
2025-05-17 02:16:41 +07:00
str := types . ToString ( val )
2024-03-14 03:08:53 +05:30
if str == "" {
// use two strings
str = "string1\nstring2"
}
if value . Schema != nil && generic . EqualsAny ( value . Schema . Value . Format , "bindary" , "byte" ) {
cloned . Body = io . NopCloser ( bytes . NewReader ( [ ] byte ( str ) ) )
cloned . ContentLength = int64 ( len ( str ) )
cloned . Header . Set ( "Content-Type" , "application/octet-stream" )
} else {
// use string placeholder
cloned . Body = io . NopCloser ( strings . NewReader ( str ) )
cloned . ContentLength = int64 ( len ( str ) )
cloned . Header . Set ( "Content-Type" , "text/plain" )
}
default :
gologger . Verbose ( ) . Msgf ( "openapi: no correct content type found for body: %s\n" , content )
// LOG: return errors.New("no correct content type found for body")
continue
}
dumped , err := httputil . DumpRequestOut ( cloned , true )
if err != nil {
return errors . Wrap ( err , "could not dump request" )
}
rr , err := httpTypes . ParseRawRequestWithURL ( string ( dumped ) , cloned . URL . String ( ) )
if err != nil {
return errors . Wrap ( err , "could not parse raw request" )
}
opts . callback ( rr )
continue
}
}
if opts . op . RequestBody != nil {
return nil
}
dumped , err := httputil . DumpRequestOut ( req , true )
if err != nil {
return errors . Wrap ( err , "could not dump request" )
}
rr , err := httpTypes . ParseRawRequestWithURL ( string ( dumped ) , req . URL . String ( ) )
if err != nil {
return errors . Wrap ( err , "could not parse raw request" )
}
opts . callback ( rr )
return nil
}
// GetGlobalParamsForSecurityRequirement returns the global parameters for a security requirement
func GetGlobalParamsForSecurityRequirement ( schema * openapi3 . T , requirement * openapi3 . SecurityRequirements ) ( [ ] * openapi3 . ParameterRef , error ) {
globalParams := openapi3 . NewParameters ( )
if len ( schema . Components . SecuritySchemes ) == 0 {
2025-08-25 15:06:58 +07:00
return nil , errkit . Newf ( "security requirements (%+v) without any security schemes found in openapi file" , schema . Security )
2024-03-14 03:08:53 +05:30
}
found := false
// this api is protected for each security scheme pull its corresponding scheme
schemaLabel :
for _ , security := range * requirement {
for name := range security {
if scheme , ok := schema . Components . SecuritySchemes [ name ] ; ok {
found = true
param , err := GenerateParameterFromSecurityScheme ( scheme )
if err != nil {
return nil , err
}
globalParams = append ( globalParams , & openapi3 . ParameterRef { Value : param } )
continue schemaLabel
}
}
if ! found && len ( security ) > 1 {
// if this is case then both security schemes are required
2025-08-25 15:06:58 +07:00
return nil , errkit . Newf ( "security requirement (%+v) not found in openapi file" , security )
2024-03-14 03:08:53 +05:30
}
}
if ! found {
2025-08-25 15:06:58 +07:00
return nil , errkit . Newf ( "security requirement (%+v) not found in openapi file" , requirement )
2024-03-14 03:08:53 +05:30
}
return globalParams , nil
}
2024-09-01 16:41:01 +08:00
// GenerateParameterFromSecurityScheme generates an example from a schema object
2024-03-14 03:08:53 +05:30
func GenerateParameterFromSecurityScheme ( scheme * openapi3 . SecuritySchemeRef ) ( * openapi3 . Parameter , error ) {
if ! generic . EqualsAny ( scheme . Value . Type , "http" , "apiKey" ) {
2025-08-25 15:06:58 +07:00
return nil , errkit . Newf ( "unsupported security scheme type (%s) found in openapi file" , scheme . Value . Type )
2024-03-14 03:08:53 +05:30
}
if scheme . Value . Type == "http" {
// check scheme
if ! generic . EqualsAny ( scheme . Value . Scheme , "basic" , "bearer" ) {
2025-08-25 15:06:58 +07:00
return nil , errkit . Newf ( "unsupported security scheme (%s) found in openapi file" , scheme . Value . Scheme )
2024-03-14 03:08:53 +05:30
}
2024-09-19 19:29:19 +05:30
// HTTP authentication schemes basic or bearer use the Authorization header
headerName := scheme . Value . Name
if headerName == "" {
headerName = DEFAULT_HTTP_SCHEME_HEADER
2024-03-14 03:08:53 +05:30
}
// create parameters using the scheme
switch scheme . Value . Scheme {
case "basic" :
2024-09-19 19:29:19 +05:30
h := openapi3 . NewHeaderParameter ( headerName )
2024-03-14 03:08:53 +05:30
h . Required = true
h . Description = globalAuth // differentiator for normal variables and global auth
return h , nil
case "bearer" :
2024-09-19 19:29:19 +05:30
h := openapi3 . NewHeaderParameter ( headerName )
2024-03-14 03:08:53 +05:30
h . Required = true
h . Description = globalAuth // differentiator for normal variables and global auth
return h , nil
}
}
if scheme . Value . Type == "apiKey" {
// validate name and in
if scheme . Value . Name == "" {
2025-08-25 15:06:58 +07:00
return nil , errkit . Newf ( "security scheme (%s) name is empty" , scheme . Value . Type )
2024-03-14 03:08:53 +05:30
}
if ! generic . EqualsAny ( scheme . Value . In , "query" , "header" , "cookie" ) {
2025-08-25 15:06:58 +07:00
return nil , errkit . Newf ( "unsupported security scheme (%s) in (%s) found in openapi file" , scheme . Value . Type , scheme . Value . In )
2024-03-14 03:08:53 +05:30
}
// create parameters using the scheme
switch scheme . Value . In {
case "query" :
q := openapi3 . NewQueryParameter ( scheme . Value . Name )
q . Required = true
q . Description = globalAuth // differentiator for normal variables and global auth
return q , nil
case "header" :
h := openapi3 . NewHeaderParameter ( scheme . Value . Name )
h . Required = true
h . Description = globalAuth // differentiator for normal variables and global auth
return h , nil
case "cookie" :
c := openapi3 . NewCookieParameter ( scheme . Value . Name )
c . Required = true
c . Description = globalAuth // differentiator for normal variables and global auth
return c , nil
}
}
2025-08-25 15:06:58 +07:00
return nil , errkit . Newf ( "unsupported security scheme type (%s) found in openapi file" , scheme . Value . Type )
2024-03-14 03:08:53 +05:30
}