Skip to content

Validate php_class_prefix and objc_class_prefix in code generators - #29130

Closed
BarakSrour wants to merge 9 commits into
protocolbuffers:mainfrom
BarakSrour:validate-class-prefix-options
Closed

BarakSrour wants to merge 9 commits into
protocolbuffers:mainfrom
BarakSrour:validate-class-prefix-options

Conversation

@BarakSrour

@BarakSrour BarakSrour commented Aug 11, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Follow-up to #28667, which added character validation for php_namespace, php_metadata_namespace, ruby_package and csharp_namespace because they are emitted directly into generated source without validation.

Two options in the same family were not covered and are still unvalidated on main:

option read at validated before this PR
php_class_prefix php/names.cc (ClassNamePrefixImpl) no
objc_class_prefix objectivec/names.cc (FileClassPrefix) no

php_class_prefix is easy to miss by inspection: the guard added in #28667 and both of its call sites are in php_generator.cc, while php_class_prefix is read from names.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:

option php_class_prefix = "Pfx{} class Injected { public static function pwn() { return 'INJECTED'; } } class ";

protoc --php_out exits 0 and the generated file contains:

class Pfx{} class Injected { public static function pwn() { return 'INJECTED'; } } class M extends \Google\Protobuf\Internal\Message

That is valid PHP (php -l: no syntax errors), the injected class is defined and its code runs, and Tp\M is still generated correctly β€” so nothing fails to build and nothing looks wrong.

Objective-C. FileClassPrefix returns the option verbatim, so with objc_class_prefix = "A\";void pwn(){}//" the value lands in @interface / @implementation declarations, a typedef struct tag 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:

objc_class_prefix = "AB"                 -> warning: Invalid 'option objc_class_prefix = "AB";'
objc_class_prefix = "A\";void pwn(){}//" -> no diagnostic at all

Changes

Notes for review

Three things I'd rather flag than have you find:

  1. 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 every objc_class_prefix in this repo (including the deliberately-empty one) and none would be rejected. Happy to switch to the absl::CharSet denylist used for PHP/Ruby/C# if you'd prefer consistency over precision.

  2. Placement matters. The character check runs in ValidateObjCClassPrefixes() over every file, ahead of both documented opt-outs (expected_prefixes_path=- and expected_prefixes_suppressions), since disabling the naming policy should not disable a correctness check. There is a regression test per opt-out.

  3. Java is gated, but to a much narrower bar β€” java/file.cc rejects only ;, \r, \n and 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 current main. 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.1 and protoc 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 its cc_test target) β€” 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.

@BarakSrour
BarakSrour requested review from a team as code owners August 11, 2026 22:18
@BarakSrour
BarakSrour requested review from bshaffer and thomasvl and removed request for a team August 11, 2026 22:18
@google-cla

google-cla Bot commented Aug 11, 2026

Copy link
Copy Markdown

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.
@BarakSrour
BarakSrour force-pushed the validate-class-prefix-options branch from 3c71f8f to c0f6424 Compare August 11, 2026 22:33
BarakSrour and others added 2 commits August 12, 2026 08:40
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 thomasvl left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The ObjC side looks good.

Someone else will have to speak to the PHP side.

@thomasvl

Copy link
Copy Markdown
Contributor

Oops, lease see the CLA bot feedback above.

@BarakSrour

Copy link
Copy Markdown
Contributor Author

Oops, lease see the CLA bot feedback above.

I'm cleared and the check passes now πŸ‘

@tonyliaoss tonyliaoss added the πŸ…°οΈ safe for tests Mark a commit as safe to run presubmits over label Aug 13, 2026
@github-actions github-actions Bot removed the πŸ…°οΈ safe for tests Mark a commit as safe to run presubmits over label Aug 13, 2026
@tonyliaoss tonyliaoss added the πŸ…°οΈ safe for tests Mark a commit as safe to run presubmits over label Aug 13, 2026
@github-actions github-actions Bot removed the πŸ…°οΈ safe for tests Mark a commit as safe to run presubmits over label Aug 13, 2026
// 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,

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks πŸ‘ Renamed to IsValidPhpGencodeOption

@tonyliaoss tonyliaoss self-assigned this Aug 14, 2026
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.
@BarakSrour
BarakSrour requested a review from zhangskz August 15, 2026 01:03
protobuf-team-bot added a commit that referenced this pull request Aug 17, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants