{"record":{"id":"6ea0c2c229d31f17","repo":"Tencent/WeKnora","slug":"storage-path-workspace-mismatch","errorCode":null,"errorMessage":"storage path workspace mismatch","messagePattern":"storage path workspace mismatch","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"warning","filePath":"internal/utils/presign.go","lineNumber":121,"sourceCode":"}\n\n// kbScopedExportsSegment is the only storage prefix served by the KB-scoped\n// file proxy. Embedded wiki/chunk images land under exports/; raw knowledge\n// uploads use {tenant}/{knowledgeID}/... and are served via\n// /knowledge/{id}/download instead.\nconst kbScopedExportsSegment = \"exports\"\n\n// ValidateStoragePathTenant ensures the tenant segment embedded in a provider://\n// storage path matches the authenticated caller's tenant. Cross-tenant access\n// for arbitrary tenant paths uses /api/v1/files/presigned with an HMAC bound to\n// the resource owner; KB-scoped shared rendering uses ValidateKBScopedStoragePath.\nfunc ValidateStoragePathTenant(filePath string, tenantID uint64) error {\n\tpathTenant := ParseTenantIDFromStoragePath(filePath)\n\tif pathTenant == 0 {\n\t\treturn fmt.Errorf(\"storage path has no tenant segment\")\n\t}\n\tif pathTenant != tenantID {\n\t\treturn fmt.Errorf(\"storage path workspace mismatch\")\n\t}\n\treturn nil\n}\n\n// ValidateKBScopedStoragePath is used by GET /knowledge-bases/:id/files. It\n// requires the path to belong to the KB owner tenant and to live under the\n// exports/ namespace used for embedded images (SaveBytes / multimodal output).\n// This prevents borrowers with shared-KB read access from using the proxy to\n// fetch arbitrary owner-tenant objects such as raw knowledge uploads.\nfunc ValidateKBScopedStoragePath(filePath string, tenantID uint64) error {\n\tif err := ValidateStoragePathTenant(filePath, tenantID); err != nil {\n\t\treturn err\n\t}\n\tif !storagePathHasExportsScope(filePath, tenantID) {\n\t\treturn fmt.Errorf(\"storage path is outside KB-scoped exports namespace\")\n\t}\n\treturn nil\n}","sourceCodeStart":103,"sourceCodeEnd":139,"githubUrl":"https://github.com/Tencent/WeKnora/blob/988cbb03305e055d8ebb7d46d9ac6cc0803cd074/internal/utils/presign.go#L103-L139","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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"],"exampleFix":"// before: trusts client-supplied tenant\nten, _ := strconv.ParseUint(r.URL.Query().Get(\"tenant\"), 10, 64)\nerr := ValidateStoragePathTenant(path, ten)\n// after: uses authenticated principal\nerr := ValidateStoragePathTenant(path, session.TenantID)","handlingStrategy":"try-catch","validationCode":"if tenantFromPath := utils.ParseTenantIDFromStoragePath(key); tenantFromPath != 0 && tenantFromPath != session.TenantID {\n    return errors.New(\"requested object belongs to another workspace\")\n}","typeGuard":"func belongsToCallerTenant(key string, tenantID uint64) bool {\n    return utils.ParseTenantIDFromStoragePath(key) == tenantID\n}","tryCatchPattern":"err := utils.ValidateStoragePathTenant(key, session.TenantID)\nif err != nil {\n    if strings.Contains(err.Error(), \"workspace mismatch\") {\n        log.Warn(\"cross-tenant storage access denied\", \"key\", key, \"caller_tenant\", session.TenantID)\n        http.Error(w, \"not found\", http.StatusNotFound) // don't leak existence\n        return\n    }\n    http.Error(w, \"bad request\", http.StatusBadRequest)\n}","preventionTips":["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"],"tags":["authorization","multitenancy","idor","presigned-url"],"backgroundTag":"cross-tenant-storage-access","analyzedSha":"988cbb03305e055d8ebb7d46d9ac6cc0803cd074","analyzedAt":"2026-09-02T14:41:08.344Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-08T10:18:20.063Z"}