Tencent/WeKnora · warning
storage path workspace mismatch
Error message
storage path workspace mismatch
What it means
ValidateStoragePathTenant parsed a tenant ID from the storage path, but it does not match the authenticated caller's tenantID. This blocks cross-tenant reads/writes through presigned storage paths: the object belongs to a different workspace than the requester. It's an authorization failure, not a malformed path.
Source
Thrown at internal/utils/presign.go:121
}
// kbScopedExportsSegment is the only storage prefix served by the KB-scoped
// file proxy. Embedded wiki/chunk images land under exports/; raw knowledge
// uploads use {tenant}/{knowledgeID}/... and are served via
// /knowledge/{id}/download instead.
const kbScopedExportsSegment = "exports"
// ValidateStoragePathTenant ensures the tenant segment embedded in a provider://
// storage path matches the authenticated caller's tenant. Cross-tenant access
// for arbitrary tenant paths uses /api/v1/files/presigned with an HMAC bound to
// the resource owner; KB-scoped shared rendering uses ValidateKBScopedStoragePath.
func ValidateStoragePathTenant(filePath string, tenantID uint64) error {
pathTenant := ParseTenantIDFromStoragePath(filePath)
if pathTenant == 0 {
return fmt.Errorf("storage path has no tenant segment")
}
if pathTenant != tenantID {
return fmt.Errorf("storage path workspace mismatch")
}
return nil
}
// ValidateKBScopedStoragePath is used by GET /knowledge-bases/:id/files. It
// requires the path to belong to the KB owner tenant and to live under the
// exports/ namespace used for embedded images (SaveBytes / multimodal output).
// This prevents borrowers with shared-KB read access from using the proxy to
// fetch arbitrary owner-tenant objects such as raw knowledge uploads.
func ValidateKBScopedStoragePath(filePath string, tenantID uint64) error {
if err := ValidateStoragePathTenant(filePath, tenantID); err != nil {
return err
}
if !storagePathHasExportsScope(filePath, tenantID) {
return fmt.Errorf("storage path is outside KB-scoped exports namespace")
}
return nil
}View on GitHub (pinned to 988cbb0330)
Solutions
- Verify the frontend isn't reusing stale/cached storage paths from a different tenant; fetch paths fresh from the API per tenant
- Ensure the caller passes the authenticated principal's tenantID (from the session/JWT), not a value taken from the request path or query string
- Use the KB-scoped flow (ValidateKBScopedStoragePath) if the resource is intentionally shared across tenants via a knowledge base
- If the object is truly misplaced, an admin should move it under the correct tenant prefix rather than relaxing the check
Example fix
// before: trusts client-supplied tenant
ten, _ := strconv.ParseUint(r.URL.Query().Get("tenant"), 10, 64)
err := ValidateStoragePathTenant(path, ten)
// after: uses authenticated principal
err := ValidateStoragePathTenant(path, session.TenantID) Defensive patterns
Strategy: try-catch
Validate before calling
if tenantFromPath := utils.ParseTenantIDFromStoragePath(key); tenantFromPath != 0 && tenantFromPath != session.TenantID {
return errors.New("requested object belongs to another workspace")
} Type guard
func belongsToCallerTenant(key string, tenantID uint64) bool {
return utils.ParseTenantIDFromStoragePath(key) == tenantID
} Try / catch
err := utils.ValidateStoragePathTenant(key, session.TenantID)
if err != nil {
if strings.Contains(err.Error(), "workspace mismatch") {
log.Warn("cross-tenant storage access denied", "key", key, "caller_tenant", session.TenantID)
http.Error(w, "not found", http.StatusNotFound) // don't leak existence
return
}
http.Error(w, "bad request", http.StatusBadRequest)
} Prevention
- Never take tenant IDs from client input; derive them from the authenticated session only
- Return 404 (not 403) on mismatch so attackers can't distinguish existing foreign objects
- Route intentional cross-tenant sharing through ValidateKBScopedStoragePath, not the raw check
- Monitor mismatch rates per tenant to spot IDOR probing and stale-cache bugs
When it happens
Trigger: A user requesting a presigned URL for a file path whose embedded tenant segment belongs to another workspace — e.g. passed-in path /storage/42/files/a.pdf while the JWT's tenantID is 7, or a shared KB file accessed outside ValidateKBScopedStoragePath's allowed tenant set.
Common situations: Copy-pasted links between users of different workspaces; client code caching absolute storage URLs from another environment (staging vs prod with differing tenant IDs); bugs where the caller passes the resource owner's tenant instead of the authenticated principal's; IDOR probing attempts.
Related errors
- storage path has no tenant segment
- rbac: resource not found
- rbac: ownership or role insufficient
- wiki page %s returned knowledge base %s while resolving allo
- failed to generate KS3 presigned URL: %w
AI-assisted analysis of Tencent/WeKnora@988cbb0330 (2026-09-02).
Data as JSON: /api/errors/6ea0c2c229d31f17.
Report an issue: GitHub.