Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
97 changes: 97 additions & 0 deletions src/google/protobuf/compiler/command_line_interface.cc
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
#include <fstream>
#include <iostream>
#include <memory>
#include <optional>
#include <ostream>
#include <string>
#include <utility>
Expand Down Expand Up @@ -1039,6 +1040,97 @@ bool HasReservedFieldNumber(const FieldDescriptor* field) {
return false;
}

bool HasDebugRedactBehavior(const EnumDescriptor& enm) {
for (int i = 0; i < enm.value_count(); ++i) {
const EnumValueDescriptor* value = enm.value(i);
if (value->options().debug_redact()) {
return true;
}
}
return false;
}

bool HasDebugRedactBehavior(
const Descriptor& desc,
absl::flat_hash_map<const Descriptor*, bool>& visited);

bool HasDebugRedactBehavior(
const FieldDescriptor& field,
absl::flat_hash_map<const Descriptor*, bool>& visited) {
// We do not check field.options().debug_redact() here because that only
// controls redaction of that specific field. The problematic case is enum
// values, which can turn *other* custom options into redaction markers.
if (field.enum_type() != nullptr &&
HasDebugRedactBehavior(*field.enum_type())) {
return true;
}
if (field.message_type() != nullptr &&
HasDebugRedactBehavior(*field.message_type(), visited)) {
return true;
}
return false;
}

bool HasDebugRedactBehavior(
const Descriptor& desc,
absl::flat_hash_map<const Descriptor*, bool>& visited) {
auto result = visited.emplace(&desc, false);
if (!result.second) {
return result.first->second;
}
for (int i = 0; i < desc.field_count(); ++i) {
if (HasDebugRedactBehavior(*desc.field(i), visited)) {
visited[&desc] = true;
return true;
}
}
return false;
}

// Look for any enums with values marked debug_redact within this message
// schema. These can be used to mark fields that need to be redacted in debug
// string APIs, but are only discoverable via reflection that doesn't force
// linkage.
std::optional<std::string> FindDebugRedactMarker(const FileDescriptor& desc) {
std::optional<std::string> debug_redact_value;
absl::flat_hash_map<const Descriptor*, bool> visited;
google::protobuf::internal::VisitDescriptors(
desc, [&debug_redact_value, &visited](const FieldDescriptor& field) {
if (field.is_extension() && HasDebugRedactBehavior(field, visited)) {
debug_redact_value = field.full_name();
}
});
return debug_redact_value;
}

bool ValidateOptionImports(const FileDescriptor& file,
const DescriptorPool& pool,
DescriptorPool::ErrorCollector* printer) {
for (int i = 0; i < file.option_dependency_count(); ++i) {
const FileDescriptor* dep =
pool.FindFileByName(file.option_dependency_name(i));
if (dep == nullptr) {
// If we don't have the dependency we can't validate it, assume it's ok.
continue;
}
std::optional<std::string> debug_redact_value = FindDebugRedactMarker(*dep);
if (debug_redact_value.has_value()) {
printer->RecordError(
file.name(), "", nullptr, DescriptorPool::ErrorCollector::OPTION_NAME,
absl::StrCat(
"Option dependency ", dep->name(), " contains a custom option ",
*debug_redact_value,
" marked debug_redact, which is used to mark fields that need to "
"be redacted in debug string APIs. Switch to a regular import or "
"remove the debug_redact annotation to avoid potentially leaking "
"sensitive data."));
return false;
}
}

return true;
}

} // namespace

namespace {
Expand Down Expand Up @@ -1294,6 +1386,11 @@ int CommandLineInterface::Run(int argc, const char* const argv[]) {
bool validation_error = false; // Defer exiting so we log more warnings.

for (auto& file : parsed_files) {
if (!ValidateOptionImports(*file, *descriptor_pool,
error_collector.get())) {
validation_error = true;
}

google::protobuf::internal::VisitDescriptors(
*file, [&](const FieldDescriptor& field) {
if (HasReservedFieldNumber(&field)) {
Expand Down
99 changes: 99 additions & 0 deletions src/google/protobuf/compiler/command_line_interface_unittest.cc
Original file line number Diff line number Diff line change
Expand Up @@ -3151,6 +3151,105 @@ TEST_F(CommandLineInterfaceTest, WriteTransitiveOptionImportDescriptorSet) {
EXPECT_EQ("bar.proto", descriptor_set.file(3).name());
}

TEST_F(CommandLineInterfaceTest, OptionImportWithDebugRedactFieldIsNotError) {
CreateTempFile("google/protobuf/descriptor.proto",
google::protobuf::DescriptorProto::descriptor()->file()->DebugString());
CreateTempFile("custom_option.proto",
R"schema(
syntax = "proto2";
import "google/protobuf/descriptor.proto";
extend .google.protobuf.FileOptions {
optional int32 file_opt = 5000 [debug_redact = true];
}
)schema");
CreateTempFile("bar.proto",
R"schema(
edition = "2024";
import option "custom_option.proto";
option (file_opt) = 1;
message Bar {
int32 foo = 1;
}
)schema");

Run("protocol_compiler --descriptor_set_out=$tmpdir/descriptor_set "
"--include_imports --proto_path=$tmpdir bar.proto "
"--experimental_editions");

ExpectNoErrors();
}

TEST_F(CommandLineInterfaceTest, OptionImportWithDebugRedactIsError) {
CreateTempFile("google/protobuf/descriptor.proto",
google::protobuf::DescriptorProto::descriptor()->file()->DebugString());
CreateTempFile("custom_option.proto",
R"schema(
syntax = "proto2";
import "google/protobuf/descriptor.proto";
enum MyEnum {
MY_ENUM_VALUE = 0 [debug_redact = true];
}
extend .google.protobuf.FieldOptions {
optional MyEnum file_opt = 5000 [debug_redact = true];
}
)schema");
CreateTempFile("bar.proto",
R"schema(
edition = "2024";
import option "custom_option.proto";
message Bar {
int32 foo = 1 [(file_opt) = MY_ENUM_VALUE];
}
)schema");

Run("protocol_compiler --descriptor_set_out=$tmpdir/descriptor_set "
"--include_imports --proto_path=$tmpdir bar.proto "
"--experimental_editions");

ExpectErrorSubstring(
"bar.proto: Option dependency custom_option.proto contains a custom "
"option file_opt marked debug_redact");
}

TEST_F(CommandLineInterfaceTest, OptionImportWithDebugRedactDeeplyNested) {
CreateTempFile("google/protobuf/descriptor.proto",
google::protobuf::DescriptorProto::descriptor()->file()->DebugString());
CreateTempFile("custom_option.proto",
R"schema(
syntax = "proto2";
import "google/protobuf/descriptor.proto";
enum MyEnum {
MY_ENUM_VALUE = 0 [debug_redact = true];
}
message Container {
optional MyEnum enm = 1;
}
message MyMessage {
optional MyMessage msg = 1;
optional Container ctr = 2;
}
extend .google.protobuf.FieldOptions {
optional MyMessage file_opt = 5000 [debug_redact = true];
}
)schema");
CreateTempFile("bar.proto",
R"schema(
edition = "2024";
import option "custom_option.proto";
message Bar {
int32 foo = 1 [(file_opt).msg = {}];
}
)schema");

Run("protocol_compiler --descriptor_set_out=$tmpdir/descriptor_set "
"--include_imports --proto_path=$tmpdir bar.proto "
"--experimental_editions");

ExpectErrorSubstring(
"bar.proto: Option dependency custom_option.proto contains a custom "
"option file_opt marked debug_redact");
}

TEST_F(CommandLineInterfaceTest, DisallowMissingOptionImportsDescriptorSetIn) {
FileDescriptorSet file_descriptor_set;

Expand Down
Loading