projectdiscovery/nuclei · error
protoset path denied: %w
Error message
protoset path denied: %w
What it means
The ProtosetFile path was rejected by protocolstate.NormalizePathWithExecutionId: by default nuclei only permits local file reads inside the nuclei-templates directory; anything outside requires the -lfa (local file access) flag. This gate keeps JS templates from reading arbitrary files on the host running the scan.
Source
Thrown at pkg/js/libs/grpc/invoke.go:102
if cfg.maxRecvMsgSize > 0 {
opts = append(opts, grpc.WithDefaultCallOptions(grpc.MaxCallRecvMsgSize(cfg.maxRecvMsgSize)))
}
_ = ctx // reserved for future dial-time hooks; connection is established lazily
return grpc.NewClient("passthrough:///"+target, opts...)
}
// descriptorSource resolves the gRPC method/message schema either from a local
// compiled protoset file (read through the local-file-access allowlist) or, when
// no protoset is provided, from server reflection over the existing connection.
// The returned cleanup func must be called once the source is no longer needed.
func descriptorSource(ctx context.Context, executionID string, cc *grpc.ClientConn, protosetFile string) (grpcurl.DescriptorSource, func(), error) {
noop := func() {}
if strings.TrimSpace(protosetFile) != "" {
// resolve through the local-file-access allowlist: unless -lfa is set,
// only files inside the nuclei-templates directory are permitted.
normalized, err := protocolstate.NormalizePathWithExecutionId(executionID, protosetFile)
if err != nil {
return nil, noop, fmt.Errorf("protoset path denied: %w", err)
}
data, err := os.ReadFile(normalized)
if err != nil {
return nil, noop, fmt.Errorf("failed to read protoset file: %w", err)
}
fds := &descriptorpb.FileDescriptorSet{}
if err := proto.Unmarshal(data, fds); err != nil {
return nil, noop, fmt.Errorf("failed to parse protoset file: %w", err)
}
src, err := grpcurl.DescriptorSourceFromFileDescriptorSet(fds)
if err != nil {
return nil, noop, fmt.Errorf("failed to build descriptor source from protoset: %w", err)
}
return src, noop, nil
}
refClient := grpcreflect.NewClientAuto(ctx, cc)
cleanup := func() { refClient.Reset() }View on GitHub (pinned to 265b3a3dec)
Solutions
- Place the protoset inside the nuclei-templates directory (or a subdirectory) and reference it with a path that stays under that root
- Run nuclei with -lfa when the descriptor must live outside the templates root
- Prefer server reflection: omit ProtosetFile entirely when the target supports gRPC reflection
Example fix
// before: absolute path outside the templates root -> denied without -lfa const o = new grpc.Options(); o.ProtosetFile = '/opt/exploits/acme.protoset'; // after: ship the protoset with the template and keep the path under the templates root o.ProtosetFile = 'grpc/acme.protoset'; // or: nuclei -lfa ... when the file must stay where it is
Defensive patterns
Strategy: validation
Validate before calling
function assertTemplateRelativePath(p) {
if (!p || p.startsWith('/') || p.startsWith('..')) {
throw new Error('protoset path must stay inside the templates root (or run nuclei with -lfa)');
}
}
assertTemplateRelativePath(o.ProtosetFile); Type guard
const isTemplatesRelativePath = (p) => !!p && !p.startsWith('/') && !p.startsWith('..'); Try / catch
try { const c = new grpc.Client(t, o); c.Connect(); }
catch (e) { if (/protoset path denied/.test(e.message || '')) { /* move file under templates root or enable -lfa */ } } Prevention
- Ship protoset files with the template, under the templates root
- Document the -lfa requirement if the descriptor must live elsewhere
- Prefer server reflection when the target supports it, avoiding local files entirely
When it happens
Trigger: opts.ProtosetFile = '/opt/exploits/acme.protoset' with -lfa unset; a relative path that resolves outside the templates root ('../../home/user/svc.protoset'); absolute paths to tooling directories.
Common situations: Template authors keeping protosets beside their tooling instead of shipping them with the template; CI running nuclei without -lfa while the author tested locally with it enabled; enterprise installs pinning templates to a custom directory the path does not account for.
Related errors
- target denied by network policy
- failed to read protoset file: %w
- headless mode (-headless) is required if -ho, -sb, -sc or -l
- both follow host redirects and follow redirects specified
- both follow redirects and disable redirects specified
AI-assisted analysis of projectdiscovery/nuclei@265b3a3dec (2026-08-15).
Data as JSON: /api/errors/8eaebd71958c32e3.
Report an issue: GitHub.