Skip to content

Commit 528caa8

Browse files
Merge pull request #6542 from roiswd/feat-openapi-direct-fuzzing
feat(openapi/swagger): direct fuzzing using target url
2 parents c32cff8 + c814128 commit 528caa8

File tree

7 files changed

+1024
-18
lines changed

7 files changed

+1024
-18
lines changed

internal/runner/runner.go

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -254,8 +254,23 @@ func New(options *types.Options) (*Runner, error) {
254254
os.Exit(0)
255255
}
256256

257+
tmpDir, err := os.MkdirTemp("", "nuclei-tmp-*")
258+
if err != nil {
259+
return nil, errors.Wrap(err, "could not create temporary directory")
260+
}
261+
runner.tmpDir = tmpDir
262+
263+
// Cleanup tmpDir only if initialization fails
264+
// On successful initialization, Close() method will handle cleanup
265+
cleanupOnError := true
266+
defer func() {
267+
if cleanupOnError && runner.tmpDir != "" {
268+
_ = os.RemoveAll(runner.tmpDir)
269+
}
270+
}()
271+
257272
// create the input provider and load the inputs
258-
inputProvider, err := provider.NewInputProvider(provider.InputOptions{Options: options})
273+
inputProvider, err := provider.NewInputProvider(provider.InputOptions{Options: options, TempDir: runner.tmpDir})
259274
if err != nil {
260275
return nil, errors.Wrap(err, "could not create input provider")
261276
}
@@ -386,10 +401,8 @@ func New(options *types.Options) (*Runner, error) {
386401
}
387402
runner.rateLimiter = utils.GetRateLimiter(context.Background(), options.RateLimit, options.RateLimitDuration)
388403

389-
if tmpDir, err := os.MkdirTemp("", "nuclei-tmp-*"); err == nil {
390-
runner.tmpDir = tmpDir
391-
}
392-
404+
// Initialization successful, disable cleanup on error
405+
cleanupOnError = false
393406
return runner, nil
394407
}
395408

pkg/input/formats/formats.go

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
77
"strings"
88

99
"github.com/projectdiscovery/nuclei/v3/pkg/input/types"
10+
"github.com/projectdiscovery/retryablehttp-go"
1011
fileutil "github.com/projectdiscovery/utils/file"
1112
"gopkg.in/yaml.v3"
1213
)
@@ -47,6 +48,16 @@ type Format interface {
4748
SetOptions(options InputFormatOptions)
4849
}
4950

51+
// SpecDownloader is an interface for downloading API specifications from URLs
52+
type SpecDownloader interface {
53+
// Download downloads the spec from the given URL and saves it to tmpDir
54+
// Returns the path to the downloaded file
55+
// httpClient is a retryablehttp.Client instance (can be nil for fallback)
56+
Download(url, tmpDir string, httpClient *retryablehttp.Client) (string, error)
57+
// SupportedExtensions returns the list of supported file extensions
58+
SupportedExtensions() []string
59+
}
60+
5061
var (
5162
DefaultVarDumpFileName = "required_openapi_params.yaml"
5263
ErrNoVarsDumpFile = errors.New("no required params file found")
Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
package openapi
2+
3+
import (
4+
"encoding/json"
5+
"fmt"
6+
"io"
7+
"net/http"
8+
"net/url"
9+
"os"
10+
"path/filepath"
11+
"strings"
12+
"time"
13+
14+
"github.com/pkg/errors"
15+
"github.com/projectdiscovery/nuclei/v3/pkg/input/formats"
16+
"github.com/projectdiscovery/retryablehttp-go"
17+
)
18+
19+
// OpenAPIDownloader implements the SpecDownloader interface for OpenAPI 3.0 specs
20+
type OpenAPIDownloader struct{}
21+
22+
// NewDownloader creates a new OpenAPI downloader
23+
func NewDownloader() formats.SpecDownloader {
24+
return &OpenAPIDownloader{}
25+
}
26+
27+
// This function downloads an OpenAPI 3.0 spec from the given URL and saves it to tmpDir
28+
func (d *OpenAPIDownloader) Download(urlStr, tmpDir string, httpClient *retryablehttp.Client) (string, error) {
29+
// Validate URL format, OpenAPI 3.0 specs are typically JSON
30+
if !strings.HasSuffix(urlStr, ".json") {
31+
return "", fmt.Errorf("URL does not appear to be an OpenAPI JSON spec")
32+
}
33+
34+
const maxSpecSizeBytes = 10 * 1024 * 1024 // 10MB
35+
36+
// Use provided httpClient or create a fallback
37+
var client *http.Client
38+
if httpClient != nil {
39+
client = httpClient.HTTPClient
40+
} else {
41+
// Fallback to simple client if no httpClient provided
42+
client = &http.Client{Timeout: 30 * time.Second}
43+
}
44+
45+
resp, err := client.Get(urlStr)
46+
if err != nil {
47+
return "", errors.Wrap(err, "failed to download OpenAPI spec")
48+
}
49+
50+
defer func() {
51+
_ = resp.Body.Close()
52+
}()
53+
54+
if resp.StatusCode != http.StatusOK {
55+
return "", fmt.Errorf("HTTP %d when downloading OpenAPI spec", resp.StatusCode)
56+
}
57+
58+
bodyBytes, err := io.ReadAll(io.LimitReader(resp.Body, maxSpecSizeBytes))
59+
if err != nil {
60+
return "", errors.Wrap(err, "failed to read response body")
61+
}
62+
63+
// Validate it's a valid JSON and has OpenAPI structure
64+
var spec map[string]interface{}
65+
if err := json.Unmarshal(bodyBytes, &spec); err != nil {
66+
return "", fmt.Errorf("downloaded content is not valid JSON: %w", err)
67+
}
68+
69+
// Check if it's an OpenAPI 3.0 spec
70+
if openapi, exists := spec["openapi"]; exists {
71+
if openapiStr, ok := openapi.(string); ok && strings.HasPrefix(openapiStr, "3.") {
72+
// Valid OpenAPI 3.0 spec
73+
} else {
74+
return "", fmt.Errorf("not a valid OpenAPI 3.0 spec (found version: %v)", openapi)
75+
}
76+
} else {
77+
return "", fmt.Errorf("not an OpenAPI spec (missing 'openapi' field)")
78+
}
79+
80+
// Extract host from URL for server configuration
81+
parsedURL, err := url.Parse(urlStr)
82+
if err != nil {
83+
return "", errors.Wrap(err, "failed to parse URL")
84+
}
85+
host := parsedURL.Host
86+
scheme := parsedURL.Scheme
87+
if scheme == "" {
88+
scheme = "https"
89+
}
90+
91+
// Add servers section if missing or empty
92+
servers, exists := spec["servers"]
93+
if !exists || servers == nil {
94+
spec["servers"] = []map[string]interface{}{{"url": scheme + "://" + host}}
95+
} else if serverList, ok := servers.([]interface{}); ok && len(serverList) == 0 {
96+
spec["servers"] = []map[string]interface{}{{"url": scheme + "://" + host}}
97+
}
98+
99+
// Marshal back to JSON
100+
modifiedJSON, err := json.Marshal(spec)
101+
if err != nil {
102+
return "", errors.Wrap(err, "failed to marshal modified spec")
103+
}
104+
105+
// Create output directory
106+
openapiDir := filepath.Join(tmpDir, "openapi")
107+
if err := os.MkdirAll(openapiDir, 0755); err != nil {
108+
return "", errors.Wrap(err, "failed to create openapi directory")
109+
}
110+
111+
// Generate filename
112+
filename := fmt.Sprintf("openapi-spec-%d.json", time.Now().Unix())
113+
filePath := filepath.Join(openapiDir, filename)
114+
115+
// Write file
116+
file, err := os.Create(filePath)
117+
if err != nil {
118+
return "", fmt.Errorf("failed to create file: %w", err)
119+
}
120+
121+
defer func() {
122+
_ = file.Close()
123+
}()
124+
125+
if _, writeErr := file.Write(modifiedJSON); writeErr != nil {
126+
_ = os.Remove(filePath)
127+
return "", errors.Wrap(writeErr, "failed to write OpenAPI spec to file")
128+
}
129+
130+
return filePath, nil
131+
}
132+
133+
// SupportedExtensions returns the list of supported file extensions for OpenAPI
134+
func (d *OpenAPIDownloader) SupportedExtensions() []string {
135+
return []string{".json"}
136+
}

0 commit comments

Comments
 (0)