Repository navigation
Validate php_class_prefix and objc_class_prefix in code generators - #29130
BarakSrour wants to merge 9 commits into
Conversation
|
Thanks for your pull request! It looks like this may be your first contribution to a Google open source project. Before we can look at your pull request, you'll need to sign a Contributor License Agreement (CLA). View this failed invocation of the CLA check for more information. For the most up to date status, view the checks section at the bottom of the pull request. |
php_class_prefix is pasted directly into `class <prefix><Name> extends ...`,
so a prefix containing `{`, `}` or `;` can close the generated declaration and
open an attacker-defined one. The result is still valid PHP and the real
message class is still emitted, so nothing fails to build.
protocolbuffers#28667 added this validation for php_namespace and php_metadata_namespace but
not for php_class_prefix, which is read from names.cc rather than
php_generator.cc.
IsValidPhpNamespace becomes IsValidPhpOption so the error names the offending
option. The message still begins "Invalid character", so the tests added in
protocolbuffers#28667 are unaffected.
objc_class_prefix is returned verbatim by FileClassPrefix and pasted into @interface/@implementation declarations, a typedef struct tag and a C string literal, so characters that are not valid in a C identifier can break out of those constructs. The check rejects anything outside [A-Za-z0-9_], which is the only set ever valid in the C identifiers this value becomes. It runs before the existing prefix logic, which can return early on the expected-prefixes path. An empty prefix remains valid, and no objc_class_prefix in this repository is affected. Adds an objectivec generator_unittest with its cc_test target, plus the file_lists.cmake entry that is normally regenerated from Bazel.
3c71f8f to
c0f6424
Compare
β¦-options # Conflicts: # src/google/protobuf/compiler/php/generator_unittest.cc
ValidateObjCClassPrefixes() has two documented opt-outs that skipped the character check: an expected_prefixes_path of "-" returns early, and a file listed in expected_prefixes_suppressions is skipped. Either one let a prefix through that can inject code into the generated sources. Extract the check into ValidateObjCClassPrefixChars() and run it over every file at the top of ValidateObjCClassPrefixes(), before both opt-outs. The character set is a correctness requirement rather than a naming policy, so disabling the policy checks should not disable it. Adds a regression test per opt-out. Both fail with the check in its previous location.
thomasvl
left a comment
There was a problem hiding this comment.
The ObjC side looks good.
Someone else will have to speak to the PHP side.
|
Oops, lease see the CLA bot feedback above. |
I'm cleared and the check passes now π |
| // Validates that a PHP namespace/metadata namespace/class prefix option does | ||
| // not contain characters that could allow code injection into generated PHP | ||
| // code. | ||
| bool IsValidPhpOption(absl::string_view value, absl::string_view option_name, |
There was a problem hiding this comment.
nit: Prefer something like IsValidPhpGencodeOption -- this validation is necessary only for PHP options that are outputted as gencode but not for all PHP options in general.
There was a problem hiding this comment.
Thanks π Renamed to IsValidPhpGencodeOption
The validation applies to the PHP options that get written into the generated code -- the namespace, metadata namespace and class prefix -- rather than to PHP options in general, so name it accordingly.
Summary
Follow-up to #28667, which added character validation for
php_namespace,php_metadata_namespace,ruby_packageandcsharp_namespacebecause they are emitted directly into generated source without validation.Two options in the same family were not covered and are still unvalidated on
main:php_class_prefixphp/names.cc(ClassNamePrefixImpl)objc_class_prefixobjectivec/names.cc(FileClassPrefix)php_class_prefixis easy to miss by inspection: the guard added in #28667 and both of its call sites are inphp_generator.cc, whilephp_class_prefixis read fromnames.cc.What happens today
PHP. The generator emits
class <php_class_prefix><Name> extends ..., so a prefix that closes the declaration and opens its own is emitted verbatim:protoc --php_outexits 0 and the generated file contains:That is valid PHP (
php -l: no syntax errors), the injected class is defined and its code runs, andTp\Mis still generated correctly β so nothing fails to build and nothing looks wrong.Objective-C.
FileClassPrefixreturns the option verbatim, so withobjc_class_prefix = "A\";void pwn(){}//"the value lands in@interface/@implementationdeclarations, atypedef structtag and the.prefix = "..."string literal. I have not built the resulting Objective-C, so I am claiming unvalidated emission here rather than a working payload.Worth noting separately: a prefix check already exists, but it validates style rather than characters, so it warns about a legitimate short prefix and says nothing about a hostile one:
Changes
IsValidPhpNamespacetoIsValidPhpOption(value, option_name, error)so the error names the offending option, and applied it tophp_class_prefixalongside the two existing options. The message still beginsInvalid character, so the tests added in Validate language-specific namespace options in code generatorsΒ #28667 are unaffected.ValidateObjCClassPrefixChars()rejects prefixes containing anything outside[A-Za-z0-9_], run fromValidateObjCClassPrefixes()over every file ahead of the expected-prefixes opt-outs.Notes for review
Three things I'd rather flag than have you find:
The Objective-C check is an allowlist, not a denylist. The prefix is pasted into C identifiers, where nothing outside
[A-Za-z0-9_]is ever valid, so an allowlist closes the class rather than chasing characters. I checked everyobjc_class_prefixin this repo (including the deliberately-empty one) and none would be rejected. Happy to switch to theabsl::CharSetdenylist used for PHP/Ruby/C# if you'd prefer consistency over precision.Placement matters. The character check runs in
ValidateObjCClassPrefixes()over every file, ahead of both documented opt-outs (expected_prefixes_path=-andexpected_prefixes_suppressions), since disabling the naming policy should not disable a correctness check. There is a regression test per opt-out.Java is gated, but to a much narrower bar β
java/file.ccrejects only;,\r,\nand space, and its comment says it checks "only a subset". Since Java accepts/**/as a whitespace substitute,java_outer_classname = "Evil/**/implements/**/Cloneable"and"Evil/*"are both emitted into the class declaration on currentmain. I could not get code execution out of that (;blocks statement injection); the demonstrated effect is generated code that will not compile. Not changed in this PR β mentioning it in case you want the bar aligned, and happy to do it in a follow-up.Also, for whatever it's worth to release planning: #28667 itself is not in any released binary yet β the validation string is absent from both
protoc 35.1andprotoc 36.0-rc2, since it landed after the v36 branch was cut.Testing
src/google/protobuf/compiler/php/generator_unittest.ccβInvalidPhpClassPrefixRejected,ValidPhpClassPrefixAccepted.src/google/protobuf/compiler/objectivec/generator_unittest.cc(new file, plus itscc_testtarget) βInvalidObjCClassPrefixRejected,InvalidObjCClassPrefixRejectedWithExpectedPrefixesDisabled,InvalidObjCClassPrefixRejectedWhenSuppressed,ValidObjCClassPrefixAccepted,ObjCClassPrefixWithUnderscoreAccepted.The two opt-out tests fail with the character check in its original location inside
ValidateObjCClassPrefix(), and pass with it moved.Both directions are covered, so the tests fail if the validation is removed and if it is made too strict.
Both tests were run locally against this branch (
bazel test //src/google/protobuf/compiler/php:generator_unittest //src/google/protobuf/compiler/objectivec:generator_unittest), and re-run with the two guards reverted to confirm they fail without the fix.