forked from TheRestartProject/restarters.net
-
Notifications
You must be signed in to change notification settings - Fork 0
Fix: Ensure Event/Volunteer Responses only Include Intended User Fields #54
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
7 commits
Select commit
Hold shift + click to select a range
753fc89
fix(model): add sensitive fields to User $hidden array
ardelato 58a2e8e
fix(security): use allowlist in expandVolunteers and remove redundant…
ardelato 75cb3a3
test(security): add regression test for sensitive field leakage
ardelato 5f80692
fix(security): drop loaded volunteer relation so allowlist actually a…
ardelato 6897d60
fix(events): keep user_skills in volunteer allowlist so skills still …
ardelato d0147dc
test(security): close lat/long leak gap in PublicEventDataLeakTest
ardelato 854bb56
refactor(events): drop redundant top-level userSkills from expandVolu…
ardelato File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,171 @@ | ||
| <?php | ||
|
|
||
| namespace Tests\Feature; | ||
|
|
||
| use App\Models\EventsUsers; | ||
| use App\Models\Group; | ||
| use App\Models\Network; | ||
| use App\Models\Party; | ||
| use App\Models\Role; | ||
| use App\Models\User; | ||
| use Illuminate\Support\Facades\Queue; | ||
| use Tests\TestCase; | ||
|
|
||
| class PublicEventDataLeakTest extends TestCase | ||
| { | ||
| // Field-name sentinels for keys that are user-only and must never be | ||
| // serialized. latitude/longitude are deliberately NOT listed here: they | ||
| // are legitimate public fields on the event itself (Party model), so the | ||
| // bare key appears in responses for the event's location. The volunteer's | ||
| // own coordinates are guarded by the numeric value sentinels below instead. | ||
| private array $sensitiveFields = [ | ||
| 'api_token', | ||
| 'calendar_hash', | ||
| 'recovery', | ||
| 'recovery_expires', | ||
| ]; | ||
|
|
||
| private function createEventWithVolunteer(array $userOverrides = []): array | ||
| { | ||
| Queue::fake(); | ||
|
|
||
| // The group must be approved, otherwise events on it are not visible to | ||
| // anonymous visitors (PartyController::view aborts with 404) and the | ||
| // public-page leak assertions below would never execute. | ||
| $group = Group::factory()->create(['approved' => true]); | ||
| $network = Network::factory()->create(); | ||
| $network->addGroup($group); | ||
|
|
||
| $event = Party::factory()->create([ | ||
| 'group' => $group, | ||
| 'event_start_utc' => '2130-01-01T12:13:00+00:00', | ||
| 'event_end_utc' => '2130-01-01T13:14:00+00:00', | ||
| ]); | ||
|
|
||
| $volunteer = User::factory()->create(array_merge([ | ||
| 'api_token' => 'SENSITIVE_TOKEN_VALUE', | ||
| 'calendar_hash' => 'SENSITIVE_CAL_HASH', | ||
| 'latitude' => 51.50741234, | ||
| 'longitude' => -0.12781234, | ||
| 'recovery' => 'SENSITIVE_RECOVERY_TOKEN', | ||
| 'recovery_expires' => now()->addHour(), | ||
| 'username' => 'SENSITIVE_USERNAME', | ||
| 'external_user_id' => 'SENSITIVE_EXT_ID', | ||
| ], $userOverrides)); | ||
|
|
||
| EventsUsers::create([ | ||
| 'event' => $event->idevents, | ||
| 'user' => $volunteer->id, | ||
| 'status' => 1, | ||
| 'role' => Role::RESTARTER, | ||
| ]); | ||
|
|
||
| return [$event, $volunteer]; | ||
| } | ||
|
|
||
| private function assertNoSensitiveFields(string $content): void | ||
| { | ||
| // Props are json_encode()d then HTML-escaped by Blade ({{ }}) on the | ||
| // public page path, so the raw content contains "api_token" | ||
| // rather than "api_token". Decode entities first, otherwise these | ||
| // field-name checks are no-ops there. | ||
| $decoded = html_entity_decode($content, ENT_QUOTES); | ||
|
|
||
| foreach ($this->sensitiveFields as $field) { | ||
| $this->assertStringNotContainsString( | ||
|
sctice-ifixit marked this conversation as resolved.
|
||
| '"' . $field . '"', | ||
| $decoded, | ||
| "Sensitive field '$field' found in response" | ||
| ); | ||
| } | ||
|
|
||
| $this->assertStringNotContainsString('SENSITIVE_TOKEN_VALUE', $content); | ||
| $this->assertStringNotContainsString('SENSITIVE_CAL_HASH', $content); | ||
| $this->assertStringNotContainsString('SENSITIVE_RECOVERY_TOKEN', $content); | ||
|
|
||
| // latitude/longitude have no string token to match on, so assert on | ||
| // their distinctive numeric values. These guard against the coordinates | ||
| // leaking even if the allowlist assertion is ever weakened. | ||
| $this->assertStringNotContainsString('51.50741234', $content, 'Volunteer latitude leaked'); | ||
| $this->assertStringNotContainsString('0.12781234', $content, 'Volunteer longitude leaked'); | ||
| } | ||
|
|
||
| /** | ||
| * Assert that every embedded "volunteer" object is limited to the | ||
| * {id, name, email} allowlist. The full User model used to leak here | ||
| * because a loaded Eloquent relation overrides the allowlist array | ||
| * during serialization, so checking only the $hidden fields above is | ||
| * not enough — username, external ids, location, age, consent dates, | ||
| * etc. would still be exposed. | ||
| */ | ||
| private function assertVolunteerObjectsAllowlistOnly(string $content): void | ||
| { | ||
| // Props are json_encode()d then HTML-escaped by Blade ({{ }}), so | ||
| // decode entities before extracting the embedded JSON. | ||
| $decoded = html_entity_decode($content, ENT_QUOTES); | ||
|
|
||
| $this->assertMatchesRegularExpression( | ||
| '/"volunteer":\{/', | ||
| $decoded, | ||
| 'No volunteer object was rendered; the allowlist assertions would be vacuous' | ||
| ); | ||
|
|
||
| preg_match_all('/"volunteer":(\{[^}]*\})/', $decoded, $matches); | ||
|
|
||
| foreach ($matches[1] as $json) { | ||
| $object = json_decode($json, true); | ||
| $this->assertIsArray($object, "Could not decode volunteer object: $json"); | ||
|
|
||
| $keys = array_keys($object); | ||
| sort($keys); | ||
|
|
||
| // The test volunteer has no skills, so user_skills is an empty array | ||
| // and the volunteer object stays flat (the regex above assumes that). | ||
| $this->assertSame( | ||
| ['email', 'id', 'name', 'user_skills'], | ||
| $keys, | ||
| 'Volunteer object must be limited to the {id, name, email, user_skills} allowlist: ' . $json | ||
| ); | ||
| } | ||
|
|
||
| // Sentinel PII values that must never reach any serialized response. | ||
| $this->assertStringNotContainsString('SENSITIVE_USERNAME', $content); | ||
| $this->assertStringNotContainsString('SENSITIVE_EXT_ID', $content); | ||
| } | ||
|
|
||
| public function testPublicEventPageDoesNotLeakSensitiveUserFields(): void | ||
| { | ||
| [$event, $volunteer] = $this->createEventWithVolunteer(); | ||
|
|
||
| $response = $this->get('/party/view/' . $event->idevents); | ||
|
|
||
| $response->assertStatus(200); | ||
| $content = $response->getContent(); | ||
|
|
||
| $this->assertNoSensitiveFields($content); | ||
| $this->assertVolunteerObjectsAllowlistOnly($content); | ||
|
|
||
| // Anonymous viewers must not see volunteer email addresses. | ||
| $this->assertStringNotContainsString( | ||
| $volunteer->email, | ||
| html_entity_decode($content, ENT_QUOTES), | ||
| 'Volunteer email address exposed to an anonymous viewer' | ||
| ); | ||
| } | ||
|
|
||
| public function testVolunteersApiDoesNotLeakSensitiveUserFields(): void | ||
| { | ||
| [$event] = $this->createEventWithVolunteer(); | ||
|
|
||
| $admin = $this->createUserWithToken(Role::ADMINISTRATOR); | ||
| $this->actingAs($admin); | ||
|
|
||
| $response = $this->get('/api/events/' . $event->idevents . '/volunteers'); | ||
|
|
||
| $response->assertStatus(200); | ||
| $content = $response->getContent(); | ||
|
|
||
| $this->assertNoSensitiveFields($content); | ||
| $this->assertVolunteerObjectsAllowlistOnly($content); | ||
| } | ||
| } | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Heads-up:
latitude,longitude, andlast_login_atweren't hidden in the Zapierchangespath (mapUserAndAuditToUserChangehid onlynumber_of_loginsand the token fields), so they were present in that feed. They're now hidden globally. No evidence anything consumes them; flagging in case a Zap maps those fields.