projectdiscovery/nuclei · error

failed to read protoset file: %w

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.

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)

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.