Summary
GET api/template-files/get-image/{fileName}.{ext} authenticates the caller but performs no per-case authorization. Any authenticated user can fetch any case image whose filename they can derive, regardless of which property or customer it belongs to.
Found while planning a compliance page that surfaces case images more widely (microting/eform-backendconfiguration-plugin#1160). Pre-existing — not introduced by that work — but that work increases exposure, so it is worth deciding on now.
The gap
eFormAPI.Web/Controllers/Eforms/EFormFilesController.cs:109-111 serves the endpoint under the class-level [Authorize] only. It takes a filename and nothing else — no case id, no property id — so there is nothing to authorize against. Any valid bearer token is sufficient for any image.
How guessable is the filename?
Not trivially, but it is derived rather than random. The 700px derivative is:
$"{UploadedData.Id}_700_{UploadedData.Checksum}{UploadedData.Extension}"
The checksum is the meaningful entropy. But note filenames are not secret in practice: they appear in API responses to any user who can read the case list, so a user with legitimate access to one property can retain and replay names, and names leak through logs, exports and browser history. This is closer to insecure-direct-object-reference than to a capability URL, and it should not be relied on as one.
Why it has not bitten
The frontend's authImage pipe (common/pipes/auth-image.pipe.ts) fetches with the bearer token and returns a base64 data: URL, so images are only ever requested for cases the UI already loaded. The gap is in what the endpoint permits, not in what the UI does.
Related, and deliberately not the fix
There is a second route, get-report-image/{name}.{ext}&token= (:116-119), which is [AllowAnonymous] and guarded by the SDK master token in the URL. That is weaker, not stronger — do not extend it or build on it.
Suggested direction
Take the case id (or the uploaded-data id) as a parameter and authorize the caller against that case's property, mirroring however case reads are already authorized. That is a contract change with frontend call sites, hence filing rather than fixing.
Worth noting for whoever picks this up: no GetPreSignedURL call exists anywhere in the SDK, so there is currently no token-free image URL other than the master-token route above. A presigned-URL approach would be a larger change than adding an authorization check.
Scope note
Server-side report generation is unaffected — WordService reads bytes from a Stream (S3 or disk) and never goes through this endpoint. This is specifically about browser-facing image fetches.
Summary
GET api/template-files/get-image/{fileName}.{ext}authenticates the caller but performs no per-case authorization. Any authenticated user can fetch any case image whose filename they can derive, regardless of which property or customer it belongs to.Found while planning a compliance page that surfaces case images more widely (microting/eform-backendconfiguration-plugin#1160). Pre-existing — not introduced by that work — but that work increases exposure, so it is worth deciding on now.
The gap
eFormAPI.Web/Controllers/Eforms/EFormFilesController.cs:109-111serves the endpoint under the class-level[Authorize]only. It takes a filename and nothing else — no case id, no property id — so there is nothing to authorize against. Any valid bearer token is sufficient for any image.How guessable is the filename?
Not trivially, but it is derived rather than random. The 700px derivative is:
$"{UploadedData.Id}_700_{UploadedData.Checksum}{UploadedData.Extension}"The checksum is the meaningful entropy. But note filenames are not secret in practice: they appear in API responses to any user who can read the case list, so a user with legitimate access to one property can retain and replay names, and names leak through logs, exports and browser history. This is closer to insecure-direct-object-reference than to a capability URL, and it should not be relied on as one.
Why it has not bitten
The frontend's
authImagepipe (common/pipes/auth-image.pipe.ts) fetches with the bearer token and returns a base64data:URL, so images are only ever requested for cases the UI already loaded. The gap is in what the endpoint permits, not in what the UI does.Related, and deliberately not the fix
There is a second route,
get-report-image/{name}.{ext}&token=(:116-119), which is[AllowAnonymous]and guarded by the SDK master token in the URL. That is weaker, not stronger — do not extend it or build on it.Suggested direction
Take the case id (or the uploaded-data id) as a parameter and authorize the caller against that case's property, mirroring however case reads are already authorized. That is a contract change with frontend call sites, hence filing rather than fixing.
Worth noting for whoever picks this up: no
GetPreSignedURLcall exists anywhere in the SDK, so there is currently no token-free image URL other than the master-token route above. A presigned-URL approach would be a larger change than adding an authorization check.Scope note
Server-side report generation is unaffected —
WordServicereads bytes from aStream(S3 or disk) and never goes through this endpoint. This is specifically about browser-facing image fetches.