{"record":{"id":"8eaebd71958c32e3","repo":"projectdiscovery/nuclei","slug":"protoset-path-denied-w","errorCode":null,"errorMessage":"protoset path denied: %w","messagePattern":"protoset path denied: %w","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"pkg/js/libs/grpc/invoke.go","lineNumber":102,"sourceCode":"\tif cfg.maxRecvMsgSize > 0 {\n\t\topts = append(opts, grpc.WithDefaultCallOptions(grpc.MaxCallRecvMsgSize(cfg.maxRecvMsgSize)))\n\t}\n\t_ = ctx // reserved for future dial-time hooks; connection is established lazily\n\treturn grpc.NewClient(\"passthrough:///\"+target, opts...)\n}\n\n// descriptorSource resolves the gRPC method/message schema either from a local\n// compiled protoset file (read through the local-file-access allowlist) or, when\n// no protoset is provided, from server reflection over the existing connection.\n// The returned cleanup func must be called once the source is no longer needed.\nfunc descriptorSource(ctx context.Context, executionID string, cc *grpc.ClientConn, protosetFile string) (grpcurl.DescriptorSource, func(), error) {\n\tnoop := func() {}\n\tif strings.TrimSpace(protosetFile) != \"\" {\n\t\t// resolve through the local-file-access allowlist: unless -lfa is set,\n\t\t// only files inside the nuclei-templates directory are permitted.\n\t\tnormalized, err := protocolstate.NormalizePathWithExecutionId(executionID, protosetFile)\n\t\tif err != nil {\n\t\t\treturn nil, noop, fmt.Errorf(\"protoset path denied: %w\", err)\n\t\t}\n\t\tdata, err := os.ReadFile(normalized)\n\t\tif err != nil {\n\t\t\treturn nil, noop, fmt.Errorf(\"failed to read protoset file: %w\", err)\n\t\t}\n\t\tfds := &descriptorpb.FileDescriptorSet{}\n\t\tif err := proto.Unmarshal(data, fds); err != nil {\n\t\t\treturn nil, noop, fmt.Errorf(\"failed to parse protoset file: %w\", err)\n\t\t}\n\t\tsrc, err := grpcurl.DescriptorSourceFromFileDescriptorSet(fds)\n\t\tif err != nil {\n\t\t\treturn nil, noop, fmt.Errorf(\"failed to build descriptor source from protoset: %w\", err)\n\t\t}\n\t\treturn src, noop, nil\n\t}\n\n\trefClient := grpcreflect.NewClientAuto(ctx, cc)\n\tcleanup := func() { refClient.Reset() }","sourceCodeStart":84,"sourceCodeEnd":120,"githubUrl":"https://github.com/projectdiscovery/nuclei/blob/265b3a3dec374741614e342f813c10f8b38d2bb7/pkg/js/libs/grpc/invoke.go#L84-L120","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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"],"exampleFix":"// before: absolute path outside the templates root -> denied without -lfa\nconst o = new grpc.Options();\no.ProtosetFile = '/opt/exploits/acme.protoset';\n\n// after: ship the protoset with the template and keep the path under the templates root\no.ProtosetFile = 'grpc/acme.protoset';\n// or: nuclei -lfa ... when the file must stay where it is","handlingStrategy":"validation","validationCode":"function assertTemplateRelativePath(p) {\n  if (!p || p.startsWith('/') || p.startsWith('..')) {\n    throw new Error('protoset path must stay inside the templates root (or run nuclei with -lfa)');\n  }\n}\nassertTemplateRelativePath(o.ProtosetFile);","typeGuard":"const isTemplatesRelativePath = (p) => !!p && !p.startsWith('/') && !p.startsWith('..');","tryCatchPattern":"try { const c = new grpc.Client(t, o); c.Connect(); }\ncatch (e) { if (/protoset path denied/.test(e.message || '')) { /* move file under templates root or enable -lfa */ } }","preventionTips":["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"],"tags":["grpc","file-access","security","configuration"],"backgroundTag":null,"analyzedSha":"265b3a3dec374741614e342f813c10f8b38d2bb7","analyzedAt":"2026-08-15T20:05:51.855Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}