VYPR
Medium severity4.3NVD Advisory· Published Jun 12, 2026

CVE-2026-47236

CVE-2026-47236

Description

Solidtime prior to 0.12.2 leaks pending invitation email addresses and member information to any organization employee via the web team page, bypassing API permission checks.

AI Insight

LLM-synthesized narrative grounded in this CVE's description and references.

Solidtime prior to 0.12.2 leaks pending invitation email addresses and member information to any organization employee via the web team page, bypassing API permission checks.

Vulnerability

In Solidtime versions prior to 0.12.2, the web team page (Jetstream-based) does not enforce the invitations:view and members:view permissions that are correctly checked on the API endpoints. Instead, it only verifies organizational membership via belongsToTeam(). As a result, any employee of an organization can access serialized team invitation emails and member details embedded in the Inertia props of the team page. The affected versions are all before 0.12.2. [1][2]

Exploitation

An attacker needs only to be a member of the target organization (any role) and have access to the web team page. No special permissions or user interaction beyond navigating to the team management page are required. The attacker can view the page source or network responses to extract the serialized invitation emails and member email addresses from the Inertia data. [2]

Impact

This vulnerability leads to unauthorized disclosure of pending invitation email addresses and current member email addresses. An attacker can enumerate potential future employees or existing staff email addresses, which could be used for targeted phishing or social engineering. No direct code execution or data modification is possible; the impact is limited to information disclosure of email addresses and associated roles. [2]

Mitigation

The issue is patched in Solidtime version 0.12.2, released on or before June 12, 2026. Users should upgrade to version 0.12.2 or later. No workarounds are available for earlier versions. The advisory [2] details the fix, which adds proper permission gates to the web team page. [1][2]

AI Insight generated on Jun 12, 2026. Synthesized from this CVE's description and the cited reference URLs; citations are validated against the source bundle.

Affected products

2

Patches

1
793bd11dcf21

remove member, invitation, and owner email disclosure from Teams/Show inertia props

https://github.com/solidtime-io/solidtimeGregor VostrakMay 18, 2026Fixed in 0.12.2via llm-release-walk
6 files changed · +47 477
  • app/Providers/JetstreamServiceProvider.php+0 20 modified
    @@ -304,28 +304,8 @@ function (Request $request, array $data): array {
                                 'owner' => [
                                     'id' => $owner->getKey(),
                                     'name' => $owner->name,
    -                                'email' => $owner->email,
                                     'profile_photo_url' => $owner->profile_photo_url,
                                 ],
    -                            'users' => $teamModel->users->map(function (User $user): array {
    -                                return [
    -                                    'id' => $user->getKey(),
    -                                    'name' => $user->name,
    -                                    'email' => $user->email,
    -                                    'profile_photo_url' => $user->profile_photo_url,
    -                                    'membership' => [
    -                                        'id' => $user->membership->id,
    -                                        'role' => $user->membership->role,
    -                                    ],
    -                                ];
    -                            }),
    -                            'team_invitations' => $teamModel->teamInvitations->map(function (OrganizationInvitation $invitation): array {
    -                                return [
    -                                    'id' => $invitation->getKey(),
    -                                    'email' => $invitation->email,
    -                                    'role' => $invitation->role,
    -                                ];
    -                            }),
                             ],
                             'currencies' => array_map(function (Currency $currency): string {
                                 return $currency->getName();
    
  • resources/js/Pages/Teams/Partials/TeamMemberManager.vue+0 448 removed
    @@ -1,448 +0,0 @@
    -<script setup lang="ts">
    -import { computed, ref } from 'vue';
    -import { router, useForm, usePage } from '@inertiajs/vue3';
    -import ActionMessage from '@/Components/ActionMessage.vue';
    -import ActionSection from '@/Components/ActionSection.vue';
    -import ConfirmationModal from '@/Components/ConfirmationModal.vue';
    -import DangerButton from '@/packages/ui/src/Buttons/DangerButton.vue';
    -import DialogModal from '@/packages/ui/src/DialogModal.vue';
    -import FormSection from '@/Components/FormSection.vue';
    -import { Field, FieldLabel, FieldError } from '@/packages/ui/src/field';
    -
    -import PrimaryButton from '@/packages/ui/src/Buttons/PrimaryButton.vue';
    -import SecondaryButton from '@/packages/ui/src/Buttons/SecondaryButton.vue';
    -import SectionBorder from '@/Components/SectionBorder.vue';
    -import TextInput from '@/packages/ui/src/Input/TextInput.vue';
    -import type { Organization, OrganizationInvitation, User } from '@/types/models';
    -import type { Membership, Permissions, Role } from '@/types/jetstream';
    -import { filterRoles } from '@/utils/roles';
    -
    -type UserWithMembership = User & { membership: Membership };
    -
    -const props = defineProps<{
    -    team: Organization;
    -    availableRoles: Role[];
    -    userPermissions: Permissions;
    -}>();
    -
    -const users = computed(() => {
    -    return props.team.users as Array<UserWithMembership>;
    -});
    -
    -const page = usePage<{
    -    auth: {
    -        user: User;
    -    };
    -}>();
    -
    -const addTeamMemberForm = useForm({
    -    email: '',
    -    role: null as string | null,
    -});
    -
    -const updateRoleForm = useForm({
    -    role: null as string | null,
    -});
    -
    -const leaveTeamForm = useForm({});
    -const removeTeamMemberForm = useForm({});
    -
    -const currentlyManagingRole = ref(false);
    -const managingRoleFor = ref<User | null>(null);
    -const confirmingLeavingTeam = ref(false);
    -const teamMemberBeingRemoved = ref<User | null>(null);
    -
    -const addTeamMember = () => {
    -    addTeamMemberForm.post(route('team-members.store', props.team.id), {
    -        errorBag: 'addTeamMember',
    -        preserveScroll: true,
    -        onSuccess: () => addTeamMemberForm.reset(),
    -    });
    -};
    -
    -const cancelTeamInvitation = (invitation: OrganizationInvitation) => {
    -    router.delete(route('team-invitations.destroy', invitation.id), {
    -        preserveScroll: true,
    -    });
    -};
    -
    -const manageRole = (teamMember: User & { membership: Membership }) => {
    -    managingRoleFor.value = teamMember;
    -    updateRoleForm.role = teamMember.membership.role;
    -    currentlyManagingRole.value = true;
    -};
    -
    -const updateRole = () => {
    -    updateRoleForm.put(
    -        route('team-members.update', {
    -            team: props.team.id,
    -            user: managingRoleFor.value?.id,
    -        }),
    -        {
    -            preserveScroll: true,
    -            onSuccess: () => (currentlyManagingRole.value = false),
    -        }
    -    );
    -};
    -
    -const confirmLeavingTeam = () => {
    -    confirmingLeavingTeam.value = true;
    -};
    -
    -const leaveTeam = () => {
    -    leaveTeamForm.delete(route('team-members.destroy', [props.team.id, page.props.auth.user.id]));
    -};
    -
    -const confirmTeamMemberRemoval = (teamMember: User) => {
    -    teamMemberBeingRemoved.value = teamMember;
    -};
    -
    -const removeTeamMember = () => {
    -    removeTeamMemberForm.delete(
    -        route('team-members.destroy', {
    -            team: props.team.id,
    -            user: teamMemberBeingRemoved.value?.id,
    -        }),
    -        {
    -            errorBag: 'removeTeamMember',
    -            preserveScroll: true,
    -            preserveState: true,
    -            onSuccess: () => (teamMemberBeingRemoved.value = null),
    -        }
    -    );
    -};
    -
    -const displayableRole = (role: string) => {
    -    return props.availableRoles.find((r) => r.key === role)?.name;
    -};
    -</script>
    -
    -<template>
    -    <div>
    -        <div v-if="userPermissions.canAddTeamMembers">
    -            <SectionBorder />
    -
    -            <!-- Add Organization Member -->
    -            <FormSection @submitted="addTeamMember">
    -                <template #title> Add Organization Member</template>
    -
    -                <template #description>
    -                    Add a new member to your organization, allowing them to collaborate with you.
    -                </template>
    -
    -                <template #form>
    -                    <div class="col-span-6">
    -                        <div class="max-w-xl text-sm text-muted">
    -                            Please provide the email address of the person you would like to add to
    -                            this organization.
    -                        </div>
    -                    </div>
    -
    -                    <!-- Member Email -->
    -                    <Field class="col-span-6 sm:col-span-4">
    -                        <FieldLabel for="email">Email</FieldLabel>
    -                        <TextInput
    -                            id="email"
    -                            v-model="addTeamMemberForm.email"
    -                            type="email"
    -                            class="block w-full" />
    -                        <FieldError v-if="addTeamMemberForm.errors.email">{{
    -                            addTeamMemberForm.errors.email
    -                        }}</FieldError>
    -                    </Field>
    -
    -                    <!-- Role -->
    -                    <div v-if="availableRoles.length > 0" class="col-span-6 lg:col-span-4">
    -                        <FieldLabel for="roles">Role</FieldLabel>
    -                        <FieldError v-if="addTeamMemberForm.errors.role">{{
    -                            addTeamMemberForm.errors.role
    -                        }}</FieldError>
    -
    -                        <div
    -                            class="relative z-0 mt-1 border border-card-border rounded-lg cursor-pointer">
    -                            <button
    -                                v-for="(role, i) in filterRoles(availableRoles)"
    -                                :key="role.key"
    -                                type="button"
    -                                class="relative px-4 py-3 inline-flex w-full rounded-lg focus:z-10 focus:outline-none focus:border-indigo-500 focus:ring-2 focus:ring-indigo-500"
    -                                :class="{
    -                                    'border-t border-card-border focus:border-none rounded-t-none':
    -                                        i > 0,
    -                                    'rounded-b-none': i != Object.keys(availableRoles).length - 1,
    -                                }"
    -                                @click="addTeamMemberForm.role = role.key">
    -                                <div
    -                                    :class="{
    -                                        'opacity-50':
    -                                            addTeamMemberForm.role &&
    -                                            addTeamMemberForm.role != role.key,
    -                                    }">
    -                                    <!-- Role Name -->
    -                                    <div class="flex items-center">
    -                                        <div
    -                                            class="text-sm text-text-primary"
    -                                            :class="{
    -                                                'font-semibold': addTeamMemberForm.role == role.key,
    -                                            }">
    -                                            {{ role.name }}
    -                                        </div>
    -
    -                                        <svg
    -                                            v-if="addTeamMemberForm.role == role.key"
    -                                            class="ms-2 h-5 w-5 text-green-400"
    -                                            xmlns="http://www.w3.org/2000/svg"
    -                                            fill="none"
    -                                            viewBox="0 0 24 24"
    -                                            stroke-width="1.5"
    -                                            stroke="currentColor">
    -                                            <path
    -                                                stroke-linecap="round"
    -                                                stroke-linejoin="round"
    -                                                d="M9 12.75L11.25 15 15 9.75M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
    -                                        </svg>
    -                                    </div>
    -
    -                                    <!-- Role Description -->
    -                                    <div class="mt-2 text-xs text-muted text-start">
    -                                        {{ role.description }}
    -                                    </div>
    -                                </div>
    -                            </button>
    -                        </div>
    -                    </div>
    -                </template>
    -
    -                <template #actions>
    -                    <ActionMessage :on="addTeamMemberForm.recentlySuccessful" class="me-3">
    -                        Added.
    -                    </ActionMessage>
    -
    -                    <PrimaryButton
    -                        :class="{ 'opacity-25': addTeamMemberForm.processing }"
    -                        :disabled="addTeamMemberForm.processing">
    -                        Add
    -                    </PrimaryButton>
    -                </template>
    -            </FormSection>
    -        </div>
    -
    -        <div v-if="team.team_invitations.length > 0 && userPermissions.canAddTeamMembers">
    -            <SectionBorder />
    -
    -            <!-- Organization Member Invitations -->
    -            <ActionSection class="mt-10 sm:mt-0">
    -                <template #title> Pending Organization Invitations</template>
    -
    -                <template #description>
    -                    These people have been invited to your organization and have been sent an
    -                    invitation email. They may join the organization by accepting the email
    -                    invitation.
    -                </template>
    -
    -                <!-- Pending Organization Member Invitation List -->
    -                <template #content>
    -                    <div class="space-y-6">
    -                        <div
    -                            v-for="invitation in team.team_invitations"
    -                            :key="invitation.id"
    -                            class="flex items-center justify-between">
    -                            <div class="text-muted">
    -                                {{ invitation.email }}
    -                            </div>
    -
    -                            <div class="flex items-center">
    -                                <!-- Cancel Organization Invitation -->
    -                                <button
    -                                    v-if="userPermissions.canRemoveTeamMembers"
    -                                    class="cursor-pointer ms-6 text-sm text-red-500 focus:outline-none"
    -                                    @click="cancelTeamInvitation(invitation)">
    -                                    Cancel
    -                                </button>
    -                            </div>
    -                        </div>
    -                    </div>
    -                </template>
    -            </ActionSection>
    -        </div>
    -
    -        <div v-if="users.length > 0">
    -            <SectionBorder />
    -
    -            <!-- Manage Organization Members -->
    -            <ActionSection class="mt-10 sm:mt-0">
    -                <template #title> Organization Members</template>
    -
    -                <template #description>
    -                    All of the people that are part of this organization.
    -                </template>
    -
    -                <!-- Organization Member List -->
    -                <template #content>
    -                    <div class="space-y-6">
    -                        <div
    -                            v-for="user in users"
    -                            :key="user.id"
    -                            class="flex items-center justify-between">
    -                            <div class="flex items-center">
    -                                <img
    -                                    class="w-8 h-8 rounded-full object-cover"
    -                                    :src="user.profile_photo_url"
    -                                    :alt="user.name" />
    -                                <div class="ms-4 text-text-primary">
    -                                    {{ user.name }}
    -                                </div>
    -                            </div>
    -
    -                            <div class="flex items-center">
    -                                <!-- Manage Organization Member Role -->
    -                                <button
    -                                    v-if="
    -                                        userPermissions.canUpdateTeamMembers &&
    -                                        availableRoles.length
    -                                    "
    -                                    class="ms-2 text-sm text-gray-400 underline"
    -                                    @click="manageRole(user)">
    -                                    {{ displayableRole(user.membership.role) }}
    -                                </button>
    -
    -                                <div
    -                                    v-else-if="availableRoles.length"
    -                                    class="ms-2 text-sm text-gray-400">
    -                                    {{ displayableRole(user.membership.role) }}
    -                                </div>
    -
    -                                <!-- Leave Organization -->
    -                                <button
    -                                    v-if="page.props.auth.user.id === user.id"
    -                                    class="cursor-pointer ms-6 text-sm text-red-500"
    -                                    @click="confirmLeavingTeam">
    -                                    Leave
    -                                </button>
    -
    -                                <!-- Remove Organization Member -->
    -                                <button
    -                                    v-else-if="userPermissions.canRemoveTeamMembers"
    -                                    class="cursor-pointer ms-6 text-sm text-red-500"
    -                                    @click="confirmTeamMemberRemoval(user)">
    -                                    Remove
    -                                </button>
    -                            </div>
    -                        </div>
    -                    </div>
    -                </template>
    -            </ActionSection>
    -        </div>
    -
    -        <!-- Role Management Modal -->
    -        <DialogModal :show="currentlyManagingRole" @close="currentlyManagingRole = false">
    -            <template #title> Manage Role</template>
    -
    -            <template #content>
    -                <div v-if="managingRoleFor">
    -                    <div
    -                        class="relative z-0 mt-1 border border-card-border rounded-lg cursor-pointer">
    -                        <button
    -                            v-for="(role, i) in availableRoles"
    -                            :key="role.key"
    -                            type="button"
    -                            class="relative px-4 py-3 inline-flex w-full rounded-lg focus:z-10 focus:outline-none focus:border-indigo-500 focus:ring-2 focus:ring-indigo-500"
    -                            :class="{
    -                                'border-t border-card-border focus:border-none rounded-t-none':
    -                                    i > 0,
    -                                'rounded-b-none': i !== Object.keys(availableRoles).length - 1,
    -                            }"
    -                            @click="updateRoleForm.role = role.key">
    -                            <div
    -                                :class="{
    -                                    'opacity-50':
    -                                        updateRoleForm.role && updateRoleForm.role !== role.key,
    -                                }">
    -                                <!-- Role Name -->
    -                                <div class="flex items-center">
    -                                    <div
    -                                        class="text-sm text-muted"
    -                                        :class="{
    -                                            'font-semibold': updateRoleForm.role === role.key,
    -                                        }">
    -                                        {{ role.name }}
    -                                    </div>
    -
    -                                    <svg
    -                                        v-if="updateRoleForm.role == role.key"
    -                                        class="ms-2 h-5 w-5 text-green-400"
    -                                        xmlns="http://www.w3.org/2000/svg"
    -                                        fill="none"
    -                                        viewBox="0 0 24 24"
    -                                        stroke-width="1.5"
    -                                        stroke="currentColor">
    -                                        <path
    -                                            stroke-linecap="round"
    -                                            stroke-linejoin="round"
    -                                            d="M9 12.75L11.25 15 15 9.75M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
    -                                    </svg>
    -                                </div>
    -
    -                                <!-- Role Description -->
    -                                <div class="mt-2 text-xs text-muted">
    -                                    {{ role.description }}
    -                                </div>
    -                            </div>
    -                        </button>
    -                    </div>
    -                </div>
    -            </template>
    -
    -            <template #footer>
    -                <SecondaryButton @click="currentlyManagingRole = false"> Cancel </SecondaryButton>
    -
    -                <PrimaryButton
    -                    class="ms-3"
    -                    :class="{ 'opacity-25': updateRoleForm.processing }"
    -                    :disabled="updateRoleForm.processing"
    -                    @click="updateRole">
    -                    Save
    -                </PrimaryButton>
    -            </template>
    -        </DialogModal>
    -
    -        <!-- Leave Organization Confirmation Modal -->
    -        <ConfirmationModal :show="confirmingLeavingTeam" @close="confirmingLeavingTeam = false">
    -            <template #title> Leave Organization</template>
    -
    -            <template #content> Are you sure you would like to leave this organization? </template>
    -
    -            <template #footer>
    -                <SecondaryButton @click="confirmingLeavingTeam = false"> Cancel </SecondaryButton>
    -
    -                <DangerButton
    -                    class="ms-3"
    -                    :class="{ 'opacity-25': leaveTeamForm.processing }"
    -                    :disabled="leaveTeamForm.processing"
    -                    @click="leaveTeam">
    -                    Leave
    -                </DangerButton>
    -            </template>
    -        </ConfirmationModal>
    -
    -        <!-- Remove Organization Member Confirmation Modal -->
    -        <ConfirmationModal :show="!!teamMemberBeingRemoved" @close="teamMemberBeingRemoved = null">
    -            <template #title> Remove Organization Member</template>
    -
    -            <template #content>
    -                Are you sure you would like to remove this person from the organization?
    -            </template>
    -
    -            <template #footer>
    -                <SecondaryButton @click="teamMemberBeingRemoved = null"> Cancel </SecondaryButton>
    -
    -                <DangerButton
    -                    class="ms-3"
    -                    :class="{ 'opacity-25': removeTeamMemberForm.processing }"
    -                    :disabled="removeTeamMemberForm.processing"
    -                    @click="removeTeamMember">
    -                    Remove
    -                </DangerButton>
    -            </template>
    -        </ConfirmationModal>
    -    </div>
    -</template>
    
  • resources/js/Pages/Teams/Partials/UpdateTeamNameForm.vue+0 3 modified
    @@ -51,9 +51,6 @@ const updateTeamName = () => {
                                 <div class="text-text-primary">
                                     {{ team.owner.name }}
                                 </div>
    -                            <div class="text-text-secondary text-sm">
    -                                {{ team.owner.email }}
    -                            </div>
                             </div>
                         </div>
                     </div>
    
  • resources/js/types/models.d.ts+1 3 modified
    @@ -22,9 +22,7 @@ export interface Organization {
         currency: string;
         created_at: string | null;
         updated_at: string | null;
    -    owner: User;
    -    users: User[];
    -    team_invitations: OrganizationInvitation[];
    +    owner: Pick<User, 'id' | 'name' | 'profile_photo_url'>;
     }
     export interface OrganizationInvitation {
         id: string;
    
  • resources/js/types/models.ts+1 3 modified
    @@ -29,9 +29,7 @@ export interface Organization {
         created_at: string | null;
         updated_at: string | null;
         // relations
    -    owner: User;
    -    users: User[];
    -    team_invitations: OrganizationInvitation[];
    +    owner: Pick<User, 'id' | 'name' | 'profile_photo_url'>;
     }
     
     export interface OrganizationInvitation {
    
  • tests/Unit/Endpoint/Web/TeamShowEndpointTest.php+45 0 added
    @@ -0,0 +1,45 @@
    +<?php
    +
    +declare(strict_types=1);
    +
    +namespace Tests\Unit\Endpoint\Web;
    +
    +use App\Models\OrganizationInvitation;
    +use App\Providers\JetstreamServiceProvider;
    +use Inertia\Testing\AssertableInertia as Assert;
    +use Laravel\Jetstream\Jetstream;
    +use PHPUnit\Framework\Attributes\CoversClass;
    +
    +#[CoversClass(JetstreamServiceProvider::class)]
    +class TeamShowEndpointTest extends EndpointTestAbstract
    +{
    +    protected function setUp(): void
    +    {
    +        Jetstream::$inertiaManager = null;
    +        parent::setUp();
    +    }
    +
    +    public function test_team_show_does_not_expose_member_roster_invitations_or_owner_email(): void
    +    {
    +        // Arrange
    +        $data = $this->createUserWithPermission([]);
    +        OrganizationInvitation::factory()->forOrganization($data->organization)->create([
    +            'email' => 'pending@example.com',
    +        ]);
    +        $this->actingAs($data->user);
    +
    +        // Act
    +        $response = $this->get('/teams/'.$data->organization->getKey());
    +
    +        // Assert
    +        $response->assertOk();
    +        $response->assertInertia(fn (Assert $page) => $page
    +            ->missing('team.users')
    +            ->missing('team.team_invitations')
    +            ->missing('team.owner.email')
    +            ->has('team.owner.id')
    +            ->has('team.owner.name')
    +            ->has('team.owner.profile_photo_url')
    +        );
    +    }
    +}
    

Vulnerability mechanics

Root cause

"The web team page authorizes access with only a `belongsToTeam()` gate and serializes pending invitations and member data into Inertia props, bypassing the `invitations:view` and `members:view` permission checks enforced on the API endpoints."

Attack vector

An attacker who is already a member of an organization (i.e., belongs to the team) can access the web team page at `/teams/<organization-id>`. The page authorizes access using only a `belongsToTeam()` gate, which any employee passes, and then serializes all pending invitation emails, member email addresses, and the owner's email into the Inertia props sent in the HTML body. The attacker can read this data from the page source even though the same user would receive a 403 Forbidden response from the dedicated API endpoints that correctly enforce `invitations:view` and `members:view` permissions [ref_id=1].

Affected code

The vulnerability exists in the Jetstream team page (`Teams/Show` Inertia page) and the `JetstreamServiceProvider.php` serialization logic. The patch removes the `TeamMemberManager.vue` component entirely and strips `users`, `team_invitations`, and `owner.email` from the Inertia props in `JetstreamServiceProvider.php`. The corresponding TypeScript type definitions (`models.d.ts` and `models.ts`) are also narrowed.

What the fix does

The patch removes the `TeamMemberManager.vue` component (which was orphaned and unused by the live UI) and strips the `users`, `team_invitations`, and `owner.email` fields from the Inertia props serialized in `JetstreamServiceProvider.php` [patch_id=5726997]. The TypeScript type definitions are updated to reflect that `owner` is now a `Pick<User, 'id' | 'name' | 'profile_photo_url'>` instead of a full `User`, and the `users` and `team_invitations` arrays are removed from the `Organization` interface. A new PHPUnit test (`TeamShowEndpointTest.php`) asserts that the team show endpoint no longer exposes these fields. This eliminates the disclosure surface without functional impact because the serialized data was not consumed by any active UI component.

Preconditions

  • authAttacker must be an authenticated member (employee) of the target organization.
  • networkAttacker must have network access to the Solidtime web application.

Generated on Jun 12, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.

References

2

News mentions

0

No linked articles in our index yet.