mirror of
https://github.com/projectdiscovery/nuclei.git
synced 2026-11-07 09:46:03 -10:00
Fix pprof/auth exposure on http-api-endpoint and harden file permissions
The experimental -http-api-endpoint server used http.ListenAndServe(addr, nil), which serves Go's global DefaultServeMux. Since net/http/pprof is blank-imported elsewhere in the binary for the separate, opt-in -enable-pprof feature, enabling -http-api-endpoint silently exposed /debug/pprof/* (process cmdline, heap/goroutine dumps, CPU profiling) on the same address. The endpoint's PUT /api/concurrency also had no authentication. Give the server its own ServeMux, add an optional -http-api-token flag (mirroring -dast-server-token) with a warning when unset, and add request timeouts. Also tighten file/directory permissions (0644->0600, 0755/0775->0750) across report exporters, the template installer/signer, custom-template downloaders, and output writers, so scan results and downloaded templates aren't left world-readable on shared systems. Co-Authored-By: Claude Sonnet 5 <[email protected]> Claude-Session: https://claude.ai/code/session_01QSCzGpK3LLQDKXtHCew5CG
This commit is contained in:
23 files changed
+77
-32
No files matched your search
@@ -381,6 +381,7 @@ on extensive configurability, massive extensibility and ease of use.`)
|
||||
flagSet.CallbackVar(resetCallback, "reset", "reset removes all nuclei configuration and data files (including nuclei-templates)"),
|
||||
flagSet.BoolVarP(&options.TlsImpersonate, "tls-impersonate", "tlsi", false, "enable experimental client hello (ja3) tls randomization"),
|
||||
flagSet.StringVarP(&options.HttpApiEndpoint, "http-api-endpoint", "hae", "", "experimental http api endpoint"),
|
||||
flagSet.StringVarP(&options.HttpApiToken, "http-api-token", "hat", "", "http api endpoint token (optional)"),
|
||||
)
|
||||
|
||||
flagSet.CreateGroup("interactsh", "interactsh",
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/projectdiscovery/gologger"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/js/compiler"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/types"
|
||||
"github.com/projectdiscovery/nuclei/v3/pkg/utils/json"
|
||||
@@ -22,6 +23,7 @@ type Concurrency struct {
|
||||
// Server represents the HTTP server that handles the concurrency settings endpoints.
|
||||
type Server struct {
|
||||
addr string
|
||||
token string
|
||||
config *types.Options
|
||||
}
|
||||
|
||||
@@ -29,19 +31,58 @@ type Server struct {
|
||||
func New(addr string, config *types.Options) *Server {
|
||||
return &Server{
|
||||
addr: addr,
|
||||
token: config.HttpApiToken,
|
||||
config: config,
|
||||
}
|
||||
}
|
||||
|
||||
// Start initializes the server and its routes, then starts listening on the specified address.
|
||||
//
|
||||
// A dedicated ServeMux is used (rather than http.DefaultServeMux via
|
||||
// http.HandleFunc/http.ListenAndServe) so this experimental endpoint never
|
||||
// accidentally exposes handlers registered on the default mux by other
|
||||
// packages (e.g. net/http/pprof, which is blank-imported for the separate,
|
||||
// opt-in -enable-pprof server).
|
||||
func (s *Server) Start() error {
|
||||
http.HandleFunc("/api/concurrency", s.handleConcurrency)
|
||||
if err := http.ListenAndServe(s.addr, nil); err != nil {
|
||||
if s.token == "" {
|
||||
gologger.Warning().Msgf("http-api-endpoint is running without a token (-http-api-token); anyone able to reach %s can read and change scan settings", s.addr)
|
||||
}
|
||||
|
||||
mux := http.NewServeMux()
|
||||
mux.HandleFunc("/api/concurrency", s.handleConcurrency)
|
||||
|
||||
var handler http.Handler = mux
|
||||
if s.token != "" {
|
||||
handler = s.tokenAuthMiddleware(handler)
|
||||
}
|
||||
|
||||
server := &http.Server{
|
||||
Addr: s.addr,
|
||||
Handler: handler,
|
||||
ReadHeaderTimeout: 10 * time.Second,
|
||||
ReadTimeout: 10 * time.Second,
|
||||
WriteTimeout: 10 * time.Second,
|
||||
IdleTimeout: 60 * time.Second,
|
||||
}
|
||||
if err := server.ListenAndServe(); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// tokenAuthMiddleware requires a matching ?token= query parameter on every
|
||||
// request when a token has been configured.
|
||||
func (s *Server) tokenAuthMiddleware(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
token := r.URL.Query().Get("token")
|
||||
if token == "" || token != s.token {
|
||||
http.Error(w, "Unauthorized", http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
next.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
|
||||
// handleConcurrency routes the request based on its method to the appropriate handler.
|
||||
func (s *Server) handleConcurrency(w http.ResponseWriter, r *http.Request) {
|
||||
switch r.Method {
|
||||
|
||||
@@ -1017,7 +1017,7 @@ func (r *Runner) displayExecutionInfo(store *loader.Store) {
|
||||
func (r *Runner) SaveResumeConfig(path string) error {
|
||||
dir := filepath.Dir(path)
|
||||
if !fileutil.FolderExists(dir) {
|
||||
if err := os.MkdirAll(dir, os.ModePerm); err != nil {
|
||||
if err := os.MkdirAll(dir, 0750); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
@@ -50,7 +50,7 @@ func NewIndex(cacheDir string) (*Index, error) {
|
||||
cacheDir = folderutil.AppCacheDirOrDefault(".nuclei-cache", config.BinaryName)
|
||||
}
|
||||
|
||||
if err := os.MkdirAll(cacheDir, 0755); err != nil {
|
||||
if err := os.MkdirAll(cacheDir, 0750); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
|
||||
@@ -46,12 +46,12 @@ func getAIGeneratedTemplates(prompt string, options *types.Options) ([]string, e
|
||||
}
|
||||
|
||||
pdcpTemplateDir := filepath.Join(config.DefaultConfig.GetTemplateDir(), "pdcp")
|
||||
if err := os.MkdirAll(pdcpTemplateDir, 0755); err != nil {
|
||||
if err := os.MkdirAll(pdcpTemplateDir, 0750); err != nil {
|
||||
return nil, errkit.Newf("Failed to create pdcp template directory: %v", err)
|
||||
}
|
||||
|
||||
templateFile := filepath.Join(pdcpTemplateDir, templateID+".yaml")
|
||||
err = os.WriteFile(templateFile, []byte(template), 0644)
|
||||
err = os.WriteFile(templateFile, []byte(template), 0600)
|
||||
if err != nil {
|
||||
return nil, errkit.Newf("Failed to generate template: %v", err)
|
||||
}
|
||||
|
||||
+2
-2
@@ -146,14 +146,14 @@ func downloadTemplate(client *azblob.Client, containerName string, path string,
|
||||
}
|
||||
|
||||
// Ensure the directory exists
|
||||
err = os.MkdirAll(filepath.Dir(outputPath), 0755)
|
||||
err = os.MkdirAll(filepath.Dir(outputPath), 0750)
|
||||
if err != nil {
|
||||
gologger.Error().Msgf("Error creating directory: %v", err)
|
||||
return err
|
||||
}
|
||||
|
||||
// Write the downloaded template to the local filesystem at the outputPath with the filename of the blob name
|
||||
err = os.WriteFile(outputPath, downloadedData.Bytes(), 0644)
|
||||
err = os.WriteFile(outputPath, downloadedData.Bytes(), 0600)
|
||||
|
||||
return err
|
||||
}
|
||||
+4
-4
@@ -72,7 +72,7 @@ func (bk *customTemplateGitLabRepo) Download(_ context.Context) {
|
||||
location := config.DefaultConfig.CustomGitLabTemplatesDirectory
|
||||
|
||||
// Ensure the CustomGitLabTemplateDirectory directory exists or create it if it doesn't yet exist
|
||||
err := os.MkdirAll(filepath.Dir(location), 0755)
|
||||
err := os.MkdirAll(filepath.Dir(location), 0750)
|
||||
if err != nil {
|
||||
gologger.Error().Msgf("Error creating directory: %v", err)
|
||||
return
|
||||
@@ -112,7 +112,7 @@ func (bk *customTemplateGitLabRepo) Download(_ context.Context) {
|
||||
}
|
||||
|
||||
// Ensure the subdirectory exists or create it if it doesn't yet exist
|
||||
err = os.MkdirAll(projectOutputPath, 0755)
|
||||
err = os.MkdirAll(projectOutputPath, 0750)
|
||||
if err != nil {
|
||||
gologger.Error().Msgf("Error creating subdirectory: %v", err)
|
||||
return
|
||||
@@ -162,7 +162,7 @@ func (bk *customTemplateGitLabRepo) Download(_ context.Context) {
|
||||
// (the previous implementation flattened everything by writing
|
||||
// only the basename, silently clobbering files with identical
|
||||
// names in different directories).
|
||||
if err := os.MkdirAll(filepath.Dir(outputPath), 0755); err != nil {
|
||||
if err := os.MkdirAll(filepath.Dir(outputPath), 0750); err != nil {
|
||||
gologger.Error().Msgf("error creating parent directory for GitLab project (%s) file: %s %s", project.Name, file.Path, err)
|
||||
return
|
||||
}
|
||||
@@ -170,7 +170,7 @@ func (bk *customTemplateGitLabRepo) Download(_ context.Context) {
|
||||
// Write the downloaded template to the local filesystem at
|
||||
// the precomputed safe output path (preserves directory
|
||||
// structure and prevents traversal).
|
||||
err = os.WriteFile(outputPath, contents, 0644)
|
||||
err = os.WriteFile(outputPath, contents, 0600)
|
||||
if err != nil {
|
||||
gologger.Error().Msgf("error writing GitLab project (%s) file: %s %s", project.Name, file.Path, err)
|
||||
return
|
||||
|
||||
Vendored
+2
-2
@@ -91,9 +91,9 @@ func downloadToFile(downloader *manager.Downloader, targetDirectory, bucket, key
|
||||
|
||||
// If empty dir in s3
|
||||
if stringsutil.HasSuffixI(key, "/") {
|
||||
return os.MkdirAll(file, 0775)
|
||||
return os.MkdirAll(file, 0750)
|
||||
}
|
||||
if err := os.MkdirAll(filepath.Dir(file), 0775); err != nil {
|
||||
if err := os.MkdirAll(filepath.Dir(file), 0750); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
|
||||
@@ -101,7 +101,7 @@ func ReadOpenAPIVarDumpFile() (*OpenAPIParamsCfgFile, error) {
|
||||
|
||||
// WriteOpenAPIVarDumpFile writes the required vars dump file
|
||||
func WriteOpenAPIVarDumpFile(vars *OpenAPIParamsCfgFile) error {
|
||||
f, err := os.OpenFile(DefaultVarDumpFileName, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0644)
|
||||
f, err := os.OpenFile(DefaultVarDumpFileName, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0600)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -104,7 +104,7 @@ func (d *OpenAPIDownloader) Download(urlStr, tmpDir string, httpClient *retryabl
|
||||
|
||||
// Create output directory
|
||||
openapiDir := filepath.Join(tmpDir, "openapi")
|
||||
if err := os.MkdirAll(openapiDir, 0755); err != nil {
|
||||
if err := os.MkdirAll(openapiDir, 0750); err != nil {
|
||||
return "", errors.Wrap(err, "failed to create openapi directory")
|
||||
}
|
||||
|
||||
|
||||
@@ -117,7 +117,7 @@ func (d *SwaggerDownloader) Download(urlStr, tmpDir string, httpClient *retryabl
|
||||
|
||||
// Create output directory
|
||||
swaggerDir := filepath.Join(tmpDir, "swagger")
|
||||
if err := os.MkdirAll(swaggerDir, 0755); err != nil {
|
||||
if err := os.MkdirAll(swaggerDir, 0750); err != nil {
|
||||
return "", errors.Wrap(err, "failed to create swagger directory")
|
||||
}
|
||||
|
||||
|
||||
@@ -541,7 +541,7 @@ func (t *TemplateManager) regenerateTemplateMetadata(dir string) error {
|
||||
|
||||
// Ensure templates directory exists (it may have been purged if empty)
|
||||
if !fileutil.FolderExists(dir) {
|
||||
if err := os.MkdirAll(dir, 0755); err != nil {
|
||||
if err := os.MkdirAll(dir, 0750); err != nil {
|
||||
return errkit.Wrapf(err, "failed to recreate templates directory %s after purge", dir)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -76,7 +76,7 @@ func UpdateIgnoreFile() error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := os.WriteFile(config.DefaultConfig.GetIgnoreFilePath(), bin, 0644); err != nil {
|
||||
if err := os.WriteFile(config.DefaultConfig.GetIgnoreFilePath(), bin, 0600); err != nil {
|
||||
return err
|
||||
}
|
||||
return config.DefaultConfig.UpdateNucleiIgnoreHash()
|
||||
|
||||
@@ -16,9 +16,9 @@ func newFileOutputWriter(file string, resume bool) (*fileWriter, error) {
|
||||
var output *os.File
|
||||
var err error
|
||||
if resume {
|
||||
output, err = os.OpenFile(file, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
|
||||
output, err = os.OpenFile(file, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0600)
|
||||
} else {
|
||||
output, err = os.Create(file)
|
||||
output, err = os.OpenFile(file, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0600)
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
||||
@@ -647,7 +647,7 @@ func (w *StandardWriter) WriteStoreDebugData(host, templateID, eventType string,
|
||||
_ = fileutil.CreateFolder(subFolder)
|
||||
}
|
||||
filename = filepath.Join(subFolder, fmt.Sprintf("%s.txt", filename))
|
||||
f, err := os.OpenFile(filename, os.O_APPEND|os.O_WRONLY|os.O_CREATE, 0644)
|
||||
f, err := os.OpenFile(filename, os.O_APPEND|os.O_WRONLY|os.O_CREATE, 0600)
|
||||
if err != nil {
|
||||
gologger.Error().Msgf("Could not open debug output file: %s", err)
|
||||
return
|
||||
|
||||
@@ -591,7 +591,7 @@ func (p *Page) Screenshot(act *Action, out ActionData) error {
|
||||
// return custom error as overwriting files is not supported
|
||||
return errkit.Newf("failed to write screenshot, file %v already exists", filePath)
|
||||
}
|
||||
err = os.WriteFile(filePath, data, 0540)
|
||||
err = os.WriteFile(filePath, data, 0600)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "could not write screenshot")
|
||||
}
|
||||
|
||||
@@ -62,7 +62,7 @@ func (exporter *Exporter) Close() error {
|
||||
}
|
||||
|
||||
// Attempt to write the JSON to file specified in options.JSONExport
|
||||
if err := os.WriteFile(exporter.options.File, obj, 0644); err != nil {
|
||||
if err := os.WriteFile(exporter.options.File, obj, 0600); err != nil {
|
||||
return errors.Wrap(err, "failed to create JSON file")
|
||||
}
|
||||
|
||||
|
||||
@@ -70,7 +70,7 @@ func (exporter *Exporter) WriteRows() error {
|
||||
var err error
|
||||
if exporter.outputFile == nil {
|
||||
// Open the JSONL file for writing and create it if it doesn't exist
|
||||
exporter.outputFile, err = os.OpenFile(exporter.options.File, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0644)
|
||||
exporter.outputFile, err = os.OpenFile(exporter.options.File, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to create JSONL file")
|
||||
}
|
||||
|
||||
@@ -43,12 +43,12 @@ func New(options *Options) (*Exporter, error) {
|
||||
}
|
||||
directory = dir
|
||||
}
|
||||
_ = os.MkdirAll(directory, 0755)
|
||||
_ = os.MkdirAll(directory, 0750)
|
||||
|
||||
// index generation header
|
||||
dataHeader := util.CreateTableHeader("Hostname/IP", "Finding", "Severity")
|
||||
|
||||
err := os.WriteFile(filepath.Join(directory, indexFileName), []byte(dataHeader), 0644)
|
||||
err := os.WriteFile(filepath.Join(directory, indexFileName), []byte(dataHeader), 0600)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -59,7 +59,7 @@ func New(options *Options) (*Exporter, error) {
|
||||
// Export exports a passed result event to markdown
|
||||
func (exporter *Exporter) Export(event *output.ResultEvent) error {
|
||||
// index file generation
|
||||
file, err := os.OpenFile(filepath.Join(exporter.directory, indexFileName), os.O_APPEND|os.O_WRONLY, 0644)
|
||||
file, err := os.OpenFile(filepath.Join(exporter.directory, indexFileName), os.O_APPEND|os.O_WRONLY, 0600)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -112,7 +112,7 @@ func (exporter *Exporter) Export(event *output.ResultEvent) error {
|
||||
dataBuilder.WriteString(format.CreateReportDescription(event, util.MarkdownFormatter{}, exporter.options.OmitRaw))
|
||||
data := dataBuilder.Bytes()
|
||||
|
||||
return os.WriteFile(filepath.Join(exporter.directory, subdirectory, filename), data, 0644)
|
||||
return os.WriteFile(filepath.Join(exporter.directory, subdirectory, filename), data, 0600)
|
||||
}
|
||||
|
||||
func createFileName(event *output.ResultEvent) string {
|
||||
|
||||
@@ -85,7 +85,7 @@ func (e *Exporter) Close() error {
|
||||
return nil
|
||||
}
|
||||
if dir := filepath.Dir(opts.File); dir != "." && dir != "" {
|
||||
if err := os.MkdirAll(dir, 0755); err != nil {
|
||||
if err := os.MkdirAll(dir, 0750); err != nil {
|
||||
return errors.Wrap(err, "could not create directory for PDF report")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -188,7 +188,7 @@ func (exporter *Exporter) Close() error {
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "failed to generate sarif report")
|
||||
}
|
||||
if err := os.WriteFile(exporter.options.File, bin, 0644); err != nil {
|
||||
if err := os.WriteFile(exporter.options.File, bin, 0600); err != nil {
|
||||
return errors.Wrap(err, "failed to create sarif file")
|
||||
}
|
||||
|
||||
|
||||
@@ -82,7 +82,7 @@ func SignTemplate(templateSigner *signer.TemplateSigner, templatePath string) er
|
||||
}
|
||||
buff := bytes.NewBuffer(content)
|
||||
buff.WriteString("\n" + signatureData)
|
||||
return os.WriteFile(templatePath, buff.Bytes(), 0644)
|
||||
return os.WriteFile(templatePath, buff.Bytes(), 0600)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -469,6 +469,8 @@ type Options struct {
|
||||
OutOfScope goflags.StringSlice
|
||||
// HttpApiEndpoint is the experimental http api endpoint
|
||||
HttpApiEndpoint string
|
||||
// HttpApiToken is the token required to authenticate against HttpApiEndpoint (optional)
|
||||
HttpApiToken string
|
||||
// InlineTargetsList holds inline multiline target list from a template profile
|
||||
InlineTargetsList string `yaml:"targets-inline,omitempty"`
|
||||
// ListTemplateProfiles lists all available template profiles
|
||||
@@ -703,6 +705,7 @@ func (options *Options) Copy() *Options {
|
||||
Scope: options.Scope,
|
||||
OutOfScope: options.OutOfScope,
|
||||
HttpApiEndpoint: options.HttpApiEndpoint,
|
||||
HttpApiToken: options.HttpApiToken,
|
||||
InlineTargetsList: options.InlineTargetsList,
|
||||
ListTemplateProfiles: options.ListTemplateProfiles,
|
||||
LoadHelperFileFunction: options.LoadHelperFileFunction,
|
||||
|
||||
Reference in new issue
Block a user