Skip to content

Commit 4bd30ae

Browse files
committed
fix(audit): record null actor for anonymous public-share downloads
Address Greptile P1: setting actorId to the file owner made every anonymous external download read as a self-download, undermining the exfiltration trail. recordAudit now accepts a null actor; the public content/inline routes record actorId=null with the owner in metadata.sharedByUserId and rely on ip/user-agent for the forensic trail. The misleading owner-attributed file_downloaded PostHog event is dropped on these anonymous paths.
1 parent 56835de commit 4bd30ae

3 files changed

Lines changed: 21 additions & 18 deletions

File tree

apps/sim/app/api/files/public/[token]/content/route.ts

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@ import { resolveServableDoc } from '@/lib/copilot/tools/server/files/doc-compile
88
import { validateDeploymentAuth } from '@/lib/core/security/deployment-auth'
99
import { generateRequestId } from '@/lib/core/utils/request'
1010
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
11-
import { captureServerEvent } from '@/lib/posthog/server'
1211
import { enforcePublicFileRateLimit } from '@/lib/public-shares/rate-limit'
1312
import { resolveActiveShareByToken } from '@/lib/public-shares/share-manager'
1413
import { downloadFile } from '@/lib/uploads/core/storage-service'
@@ -78,9 +77,12 @@ export const GET = withRouteHandler(
7877

7978
logger.info('Public shared file served', { token, key: file.key, size: buffer.length })
8079

80+
// Anonymous external access: the actor is genuinely unknown, so the actor
81+
// FK is null (not the owner — that would read as a self-download) and the
82+
// share owner is captured in metadata. ip/user-agent carry the trail.
8183
recordAudit({
8284
workspaceId: file.workspaceId ?? null,
83-
actorId: file.userId,
85+
actorId: null,
8486
action: AuditAction.FILE_DOWNLOADED,
8587
resourceType: AuditResourceType.FILE,
8688
resourceId: file.id,
@@ -89,17 +91,12 @@ export const GET = withRouteHandler(
8991
metadata: {
9092
access: 'public_share',
9193
anonymous: true,
94+
sharedByUserId: file.userId,
9295
fileName: file.originalName,
9396
bytes: buffer.length,
9497
},
9598
request,
9699
})
97-
captureServerEvent(
98-
file.userId,
99-
'file_downloaded',
100-
{ workspace_id: file.workspaceId ?? '', is_bulk: false, file_count: 1 },
101-
file.workspaceId ? { groups: { workspace: file.workspaceId } } : undefined
102-
)
103100

104101
// Revalidate every request: a shared file can be unshared, edited, or deleted,
105102
// so the fixed token URL must never serve stale bytes from a long-lived cache.

apps/sim/app/api/files/public/[token]/inline/route.ts

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@ import {
1111
import { validateDeploymentAuth } from '@/lib/core/security/deployment-auth'
1212
import { generateRequestId } from '@/lib/core/utils/request'
1313
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
14-
import { captureServerEvent } from '@/lib/posthog/server'
1514
import { enforcePublicFileRateLimit } from '@/lib/public-shares/rate-limit'
1615
import { resolveActiveShareByToken } from '@/lib/public-shares/share-manager'
1716
import { downloadFile } from '@/lib/uploads/core/storage-service'
@@ -93,22 +92,23 @@ export const GET = withRouteHandler(
9392
// record a spurious download.
9493
const response = await serveInlineImage(image, { sniff: true })
9594

95+
// Anonymous external access: null actor FK (not the owner), share owner in
96+
// metadata, ip/user-agent carry the trail.
9697
recordAudit({
9798
workspaceId: doc.workspaceId,
98-
actorId: doc.userId,
99+
actorId: null,
99100
action: AuditAction.FILE_DOWNLOADED,
100101
resourceType: AuditResourceType.FILE,
101102
resourceName: image.filename,
102103
description: `Public share inline image "${image.filename}"`,
103-
metadata: { access: 'public_share', anonymous: true, inline: true },
104+
metadata: {
105+
access: 'public_share',
106+
anonymous: true,
107+
inline: true,
108+
sharedByUserId: doc.userId,
109+
},
104110
request,
105111
})
106-
captureServerEvent(
107-
doc.userId,
108-
'file_downloaded',
109-
{ workspace_id: doc.workspaceId, is_bulk: false, file_count: 1 },
110-
{ groups: { workspace: doc.workspaceId } }
111-
)
112112

113113
return response
114114
} catch (error) {

packages/audit/src/log.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,13 @@ const logger = createLogger('AuditLog')
88

99
interface AuditLogParams {
1010
workspaceId?: string | null
11-
actorId: string
11+
/**
12+
* The acting user's id (FK to `user.id`). Pass `null` for genuinely
13+
* actor-less events such as anonymous public-share access — the row is then
14+
* persisted with a null actor and the forensic context (ip/user-agent,
15+
* metadata) carries the trail instead.
16+
*/
17+
actorId: string | null
1218
action: AuditActionType
1319
resourceType: AuditResourceTypeValue
1420
resourceId?: string

0 commit comments

Comments
 (0)