From ec2a643dd9010ef0df7d82fc6c17496737bca17a Mon Sep 17 00:00:00 2001 From: smarcet Date: Wed, 26 Aug 2026 12:11:56 -0300 Subject: [PATCH 1/2] fix(speakers): stop exposing raw phone_number in Public-context speaker serialization PresentationSpeakerSerializer::checkDataPermissions() only masked phone_number when the target speaker's own account-level public_profile_show_telephone_number toggle was off. Any authenticated non-admin caller (e.g. the CFP co-speaker search/autocomplete via GET /api/v1/summits/{id}/speakers or /api/v1/speakers) could retrieve a speaker's raw phone number whenever that toggle happened to be on, regardless of the caller's identity or relationship to the speaker. Per policy/profile-data-handling.md Rule 4, phone number is never public. Blank it unconditionally in the Public-serializer path instead of gating it on the target's own toggle. AdminPresentationSpeakerSerializer (admin views, "my own profile") overrides checkDataPermissions as a no-op and is unaffected. ClickUp: https://app.clickup.com/t/86bbkjxr0 --- .../PresentationSpeakerSerializer.php | 7 ++- tests/PresentationSpeakerSerializerTest.php | 51 +++++++++++++++++++ 2 files changed, 54 insertions(+), 4 deletions(-) create mode 100644 tests/PresentationSpeakerSerializerTest.php diff --git a/app/ModelSerializers/Summit/Speakers/PresentationSpeakerSerializer.php b/app/ModelSerializers/Summit/Speakers/PresentationSpeakerSerializer.php index 2e7476088..fdc018f3a 100644 --- a/app/ModelSerializers/Summit/Speakers/PresentationSpeakerSerializer.php +++ b/app/ModelSerializers/Summit/Speakers/PresentationSpeakerSerializer.php @@ -394,10 +394,9 @@ protected function checkDataPermissions(PresentationSpeaker $speaker, array $val } */ - if(!$speaker->isPublicProfileShowTelephoneNumber()) - { - if(isset($values['phone_number'])) $values['phone_number'] = ''; - } + // phone_number is never public regardless of the target speaker's own account + // visibility toggle - see policy/profile-data-handling.md Rule 4. + if(isset($values['phone_number'])) $values['phone_number'] = ''; return $values; } diff --git a/tests/PresentationSpeakerSerializerTest.php b/tests/PresentationSpeakerSerializerTest.php new file mode 100644 index 000000000..4bc54dd44 --- /dev/null +++ b/tests/PresentationSpeakerSerializerTest.php @@ -0,0 +1,51 @@ +makePartial(); + $speaker->shouldReceive('hasMember')->andReturn(false); + $speaker->shouldReceive('getPhoneNumber')->andReturn('+1-555-0100'); + // target speaker's own account-level toggle is ON - the exact case that used to leak + // the raw phone_number regardless of the requesting caller's identity. + $speaker->shouldReceive('isPublicProfileShowTelephoneNumber')->andReturn(true); + $speaker->shouldReceive('isPublicProfileShowBio')->andReturn(true); + $speaker->shouldReceive('isPublicProfileShowEmail')->andReturn(true); + $speaker->shouldReceive('isPublicProfileShowSocialMediaInfo')->andReturn(true); + $speaker->shouldReceive('isPublicProfileShowPhoto')->andReturn(true); + + $resource_server_context = Mockery::mock(IResourceServerContext::class); + $serializer = new PresentationSpeakerSerializer($speaker, $resource_server_context); + + $values = $serializer->serialize(null, ['phone_number'], ['none']); + + $this->assertSame('', $values['phone_number']); + } +} From 9663c2d22db4bc2bbcb2bf31b7f37c97d49f2a8e Mon Sep 17 00:00:00 2001 From: smarcet Date: Wed, 26 Aug 2026 13:10:50 -0300 Subject: [PATCH 2/2] test(speakers): call parent::tearDown() in PresentationSpeakerSerializerTest Addresses CodeRabbit review comment on PR #591 - the class-defined tearDown() stopped after Mockery::close(), skipping the inherited Laravel test cleanup. --- tests/PresentationSpeakerSerializerTest.php | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/PresentationSpeakerSerializerTest.php b/tests/PresentationSpeakerSerializerTest.php index 4bc54dd44..192a02184 100644 --- a/tests/PresentationSpeakerSerializerTest.php +++ b/tests/PresentationSpeakerSerializerTest.php @@ -26,6 +26,7 @@ final class PresentationSpeakerSerializerTest extends TestCase public function tearDown(): void { Mockery::close(); + parent::tearDown(); } public function testPhoneNumberIsMaskedInPublicContextEvenWhenSpeakerToggleIsOn()