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:
Claude committed 2026-08-03 05:54:23 +00:00
1 parent 06771d7197
commit a20aa26024
23 files changed
+77 -32

No files matched your search

+1
View File
@@ -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",
+43 -2
View File
@@ -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 {
+1 -1
View File
@@ -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
}
}
+1 -1
View File
@@ -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
}
+2 -2
View File
@@ -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
View File
@@ -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
View File
@@ -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
+2 -2
View File
@@ -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
}
+1 -1
View File
@@ -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
}
+1 -1
View File
@@ -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")
}
+1 -1
View File
@@ -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")
}
+1 -1
View File
@@ -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)
}
}
+1 -1
View File
@@ -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()
+2 -2
View File
@@ -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
+1 -1
View File
@@ -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")
}
+1 -1
View 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")
}
+4 -4
View 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 {
+1 -1
View File
@@ -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")
}
}
+1 -1
View File
@@ -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")
}
+1 -1
View 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
}
+3
View File
@@ -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,