close
Skip to content

Commit 3bf2b07

Browse files
Fail writing files in protoc CLI if any file output path is relative.
A flag --unsafe_allow_out_dir_escape is added which disables the enforcement for any users who are intentionally writing files outside of the out_dir as set in protoc. This was always the intended behavior but was not enforced before now. This avoids risks of files being accidentally written which escape the intended output directories. Strictly speaking this is more restrictive than necessary, where a relative path of `x/../y.java` could be technically ok since it doesn't escape while `x/../../y.java` does). But as we should should never have relative path components at this point in a working as intended flow so the simplest thing is to ban all relative paths when going to write out. When investigating this issue, at least one case of someone intentionally escaping the output directory by (for example) setting `go_package = "../xyz";` Unfortunately, this change will break those rare usecases and the only fix will be to adjust the .proto file and protoc invocation context, but as a security hardening measure of an unintended behavior this is an intended change. PiperOrigin-RevId: 893098701
1 parent 12c8e9b commit 3bf2b07

5 files changed

Lines changed: 77 additions & 6 deletions

File tree

‎php/BUILD.bazel‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
#
33
# See also code generation logic under /src/google/protobuf/compiler/php.
44

5-
load("@rules_pkg//pkg:mappings.bzl", "pkg_filegroup", "pkg_files", "strip_prefix")
5+
load("@rules_pkg//pkg:mappings.bzl", "pkg_filegroup", "pkg_files")
66
load("@rules_pkg//pkg:tar.bzl", "pkg_tar")
77
load("//:protobuf_version.bzl", "PROTOBUF_PHP_VERSION", "PROTOC_VERSION")
88
load("//build_defs:internal_shell.bzl", "inline_sh_binary")
@@ -175,7 +175,7 @@ genrule(
175175
"generated/ext/google/protobuf/wkt.inc",
176176
],
177177
cmd = """
178-
$(execpath //:protoc) --php_out=internal_generate_c_wkt:$(RULEDIR)/generated/src --proto_path=src $(locations //src/google/protobuf:well_known_type_protos);
178+
$(execpath //:protoc) --php_out=internal_generate_c_wkt:$(RULEDIR)/generated/src --unsafe_allow_out_dir_escape --proto_path=src $(locations //src/google/protobuf:well_known_type_protos);
179179
$(execpath //:protoc) --php_out=internal:$(RULEDIR)/generated/src --proto_path=src $(location //src/google/protobuf:descriptor_proto_srcs);
180180
""" + _CHECK_GENCODE,
181181
tags = ["manual"],

‎php/generate_descriptor_protos.sh‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ fi
2424
pushd src
2525
$PROTOC --php_out=internal:../php/src google/protobuf/descriptor.proto
2626
$PROTOC --php_out=internal_generate_c_wkt:../php/src \
27+
--unsafe_allow_out_dir_escape \
2728
google/protobuf/any.proto \
2829
google/protobuf/api.proto \
2930
google/protobuf/duration.proto \

‎src/google/protobuf/compiler/command_line_interface.cc‎

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -462,7 +462,7 @@ class CommandLineInterface::GeneratorContextImpl : public GeneratorContext {
462462

463463
// Write all files in the directory to disk at the given output location,
464464
// which must end in a '/'.
465-
bool WriteAllToDisk(const std::string& prefix);
465+
bool WriteAllToDisk(const std::string& prefix, bool allow_escape = false);
466466

467467
// Write the contents of this directory to a ZIP-format archive with the
468468
// given name.
@@ -562,7 +562,7 @@ CommandLineInterface::GeneratorContextImpl::GeneratorContextImpl(
562562
: parsed_files_(parsed_files), had_error_(false) {}
563563

564564
bool CommandLineInterface::GeneratorContextImpl::WriteAllToDisk(
565-
const std::string& prefix) {
565+
const std::string& prefix, bool allow_escape) {
566566
if (had_error_) {
567567
return false;
568568
}
@@ -576,6 +576,15 @@ bool CommandLineInterface::GeneratorContextImpl::WriteAllToDisk(
576576
const char* data = pair.second.data();
577577
int size = pair.second.size();
578578

579+
if (!allow_escape && absl::StrContains(relative_filename, "..")) {
580+
std::cerr << "Output file names must never have a relative path."
581+
<< " (" << relative_filename << "). "
582+
<< "Use --unsafe_allow_out_dir_escape to disable this error if "
583+
"intentional."
584+
<< std::endl;
585+
return false;
586+
}
587+
579588
if (!TryCreateParentDirectory(prefix, relative_filename)) {
580589
return false;
581590
}
@@ -1481,7 +1490,7 @@ int CommandLineInterface::Run(int argc, const char* const argv[]) {
14811490
const std::string& location = pair.first;
14821491
GeneratorContextImpl* directory = pair.second.get();
14831492
if (absl::EndsWith(location, "/")) {
1484-
if (!directory->WriteAllToDisk(location)) {
1493+
if (!directory->WriteAllToDisk(location, unsafe_allow_out_dir_escape_)) {
14851494
return 1;
14861495
}
14871496
} else {
@@ -2154,7 +2163,8 @@ bool CommandLineInterface::ParseArgument(const char* arg, std::string* name,
21542163
*name == "--experimental_editions" ||
21552164
*name == "--print_free_field_numbers" ||
21562165
*name == "--experimental_allow_proto3_optional" ||
2157-
*name == "--deterministic_output" || *name == "--fatal_warnings") {
2166+
*name == "--deterministic_output" ||
2167+
*name == "--unsafe_allow_out_dir_escape" || *name == "--fatal_warnings") {
21582168
// HACK: These are the only flags that don't take a value.
21592169
// They probably should not be hard-coded like this but for now it's
21602170
// not worth doing better.
@@ -2390,6 +2400,8 @@ CommandLineInterface::InterpretArgument(const std::string& name,
23902400

23912401
} else if (name == "--disallow_services") {
23922402
disallow_services_ = true;
2403+
} else if (name == "--unsafe_allow_out_dir_escape") {
2404+
unsafe_allow_out_dir_escape_ = true;
23932405
} else if (name == "--experimental_allow_proto3_optional") {
23942406
// Flag is no longer observed, but we allow it for backward compat.
23952407
} else if (name == "--encode" || name == "--decode" ||
@@ -2627,6 +2639,9 @@ Parse PROTO_FILES and generate output based on the options given:
26272639
deterministically ordered. Note that this order
26282640
is not canonical, and changes across builds or
26292641
releases of protoc.
2642+
--unsafe_allow_out_dir_escape
2643+
Allow output files to use ".." to escape the
2644+
output directory. Use with caution.
26302645
--decode=MESSAGE_TYPE Read a binary message of the given type from
26312646
standard input and write it in text format
26322647
to standard output. The message type must

‎src/google/protobuf/compiler/command_line_interface.h‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -500,6 +500,9 @@ class PROTOC_EXPORT CommandLineInterface {
500500

501501
// When using --encode, this will be passed to SetSerializationDeterministic.
502502
bool deterministic_output_ = false;
503+
// Whether to allow files to be written to a path that is outside of the
504+
// output directory.
505+
bool unsafe_allow_out_dir_escape_ = false;
503506

504507
bool opensource_runtime_ = google::protobuf::internal::IsOss();
505508

‎src/google/protobuf/compiler/command_line_interface_unittest.cc‎

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1175,6 +1175,58 @@ TEST_F(CommandLineInterfaceTest, CreateDirectory) {
11751175
ExpectGenerated("test_plugin", "", "bar/baz/foo.proto", "Foo", "plugout");
11761176
}
11771177

1178+
TEST_F(CommandLineInterfaceTest, RejectDotDotInFilename) {
1179+
class DotDotGenerator : public CodeGenerator {
1180+
public:
1181+
bool Generate(const FileDescriptor* file, const std::string& parameter,
1182+
GeneratorContext* context,
1183+
std::string* error) const override {
1184+
std::unique_ptr<io::ZeroCopyOutputStream> output(
1185+
context->Open("abc/../../foo.proto.test"));
1186+
return true;
1187+
}
1188+
};
1189+
1190+
RegisterGenerator("--dotdot_out", std::make_unique<DotDotGenerator>(),
1191+
"Test .. rejection.");
1192+
1193+
CreateTempFile("foo.proto",
1194+
"syntax = \"proto2\";\n"
1195+
"message Foo {}\n");
1196+
1197+
RunProtoc(
1198+
"protocol_compiler --dotdot_out=$tmpdir "
1199+
"--proto_path=$tmpdir foo.proto");
1200+
1201+
ExpectErrorSubstring("Output file names must never have a relative path.");
1202+
}
1203+
1204+
TEST_F(CommandLineInterfaceTest, AllowDotDotInFilenameWithFlag) {
1205+
class DotDotGenerator : public CodeGenerator {
1206+
public:
1207+
bool Generate(const FileDescriptor* file, const std::string& parameter,
1208+
GeneratorContext* context,
1209+
std::string* error) const override {
1210+
std::unique_ptr<io::ZeroCopyOutputStream> output(
1211+
context->Open("abc/../../foo.proto.test"));
1212+
return true;
1213+
}
1214+
};
1215+
1216+
RegisterGenerator("--dotdot_out", std::make_unique<DotDotGenerator>(),
1217+
"Test .. allowance.");
1218+
1219+
CreateTempFile("foo.proto",
1220+
"syntax = \"proto2\";\n"
1221+
"message Foo {}\n");
1222+
1223+
RunProtoc(
1224+
"protocol_compiler --unsafe_allow_out_dir_escape --dotdot_out=$tmpdir "
1225+
"--proto_path=$tmpdir foo.proto");
1226+
1227+
ExpectNoErrors();
1228+
}
1229+
11781230
TEST_F(CommandLineInterfaceTest, GeneratorParameters) {
11791231
// Test that generator parameters are correctly parsed from the command line.
11801232

0 commit comments

Comments
 (0)