projectdiscovery/nuclei · error

failed to read protoset file

Error message

failed to read protoset file: %w

What it means

The protoset path passed the local-file-access allowlist check, but os.ReadFile failed: the file does not exist at the normalized path, or the nuclei process lacks read permission on it. The underlying OS error is wrapped verbatim.

Solutions

  1. Verify the file exists under the templates root and fix the path/filename
  2. Regenerate the descriptor set: protoc --descriptor_set_out=acme.protoset --include_imports acme.proto
  3. Check read permissions for the user running nuclei (frequent in containers)

Example fix

# before: referenced asset never shipped
o.ProtosetFile = 'acme.protoset'; # -> failed to read protoset file

# after: generate and place under the templates root first
protoc --descriptor_set_out=nuclei-templates/grpc/acme.protoset --include_imports acme.proto
# then in the template
o.ProtosetFile = 'grpc/acme.protoset';
Defensive patterns

Strategy: validation

Validate before calling

# host-side pre-flight before running the template
test -f "${NUCLEI_TEMPLATES_DIR}/grpc/acme.protoset" || \
  protoc --descriptor_set_out="${NUCLEI_TEMPLATES_DIR}/grpc/acme.protoset" --include_imports acme.proto

Try / catch

try { const c = new grpc.Client(t, o); c.Connect(); }
catch (e) { if (/failed to read protoset/.test(e.message || '')) { /* check path and permissions, regenerate the file */ } }

Prevention

When it happens

Trigger: opts.ProtosetFile = 'grpc/acme.protoset' when the file was never added to the templates tree; wrong filename or extension (.proto vs .protoset); file present but unreadable due to permissions or a broken container volume mount.

Common situations: Template distributed without its protoset asset; partially cloned templates directory; protoc output written to a different folder than the one referenced; CI running as an unprivileged user.

Related errors


AI-assisted analysis of projectdiscovery/nuclei@265b3a3dec (2026-08-15). Data as JSON: /api/errors/bab9062a1d8cffc2. Report an issue: GitHub.

Appendix: source

Thrown at pkg/js/libs/grpc/invoke.go:106

	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() }
	return grpcurl.DescriptorSourceFromServer(ctx, refClient), cleanup, nil
}

// invokeUnary invokes a unary (or single-response) gRPC method described by src

View on GitHub (pinned to 265b3a3dec)