Summary
sendFile() documents (docblock + file-uploads guide line 198): "Directory outside of the web root where the file exists. Must be a full path." The path-resolution block in vendor/wheels/controller/miscellaneous.cfc (~lines 254-290) breaks that contract two ways, same root cause: it post-processes an already-absolute directory argument instead of using it verbatim.
Repro (guide-behavioral-audit harness, develop post-4.0.3)
Defect 1 — Adobe CF: absolute directory gets webroot-prefixed. When fullPath doesn't contain the web root, the #873 branch calls ExpandPath(local.fullPath). Lucee's ExpandPath() returns existing absolute paths unchanged, so it happens to work there; Adobe's always resolves against the web root.
// container: /tmp/dlprobe/secret.txt exists; FileExists() = true
sendFile(file="secret.txt", directory="/tmp/dlprobe");
- Lucee 7.0.1+100: 200, file served,
Content-Disposition: attachment.
- Adobe 2023.0.11:
Wheels.FileNotFound — looked in /wheels-test-suite/public/tmp/dlprobe (probe confirmed ExpandPath("/tmp/dlprobe") webroot-prefixes on Adobe). The documented serve-from-outside-the-webroot feature does not work at all on Adobe CF.
Defect 2 — all engines: /wheels substring hijack. Before the ExpandPath fallback, the resolver does findNoCase("/wheels", local.fullPath) to support the /wheels mapping — and substring-matches ANY directory containing /wheels:
sendFile(file="secret.txt", directory="/tmp/wheels-dl"); // Lucee 7
// → Wheels.FileNotFound: looked in /wheels-test-suite/public/wheels-dl
Real-world installs under /var/www/wheels/uploads or /srv/wheelsapp/files get their paths silently rewritten.
Root cause
vendor/wheels/controller/miscellaneous.cfc ~254-290: the #873 "DOES NOT CONTAIN root → ExpandPath" branch plus the findNoCase("/wheels", fullPath) substring match, both applied to caller-supplied absolute paths.
Proposed direction
When arguments.directory is supplied and is an absolute path (e.g. DirectoryExists() or starts with / / drive letter), build fullPath = directory & "/" & file and skip both the /wheels mapping rewrite and the ExpandPath() fallback. Keep existing behavior for relative filePath-based resolution. The existing ..-traversal guard runs before this block and still applies.
Acceptance
sendFile(file=..., directory="/abs/outside/webroot") serves the file on Lucee AND Adobe.
- A directory containing
/wheels as a substring is not rewritten.
- Specs cover both shapes; docs caution in
file-uploads-and-downloads.mdx line 198 removed once fixed.
Reported by the guide-behavioral-audit P1 batch 2 (work item p1-8-uploads, claim C14; live on wheels-test-lucee7:v1.0.0 + wheels-test-adobe2023:v1.0.1).
Summary
sendFile()documents (docblock + file-uploads guide line 198): "Directory outside of the web root where the file exists. Must be a full path." The path-resolution block invendor/wheels/controller/miscellaneous.cfc(~lines 254-290) breaks that contract two ways, same root cause: it post-processes an already-absolutedirectoryargument instead of using it verbatim.Repro (guide-behavioral-audit harness, develop post-4.0.3)
Defect 1 — Adobe CF: absolute directory gets webroot-prefixed. When
fullPathdoesn't contain the web root, the#873branch callsExpandPath(local.fullPath). Lucee'sExpandPath()returns existing absolute paths unchanged, so it happens to work there; Adobe's always resolves against the web root.Content-Disposition: attachment.Wheels.FileNotFound— looked in/wheels-test-suite/public/tmp/dlprobe(probe confirmedExpandPath("/tmp/dlprobe")webroot-prefixes on Adobe). The documented serve-from-outside-the-webroot feature does not work at all on Adobe CF.Defect 2 — all engines:
/wheelssubstring hijack. Before the ExpandPath fallback, the resolver doesfindNoCase("/wheels", local.fullPath)to support the/wheelsmapping — and substring-matches ANY directory containing/wheels:Real-world installs under
/var/www/wheels/uploadsor/srv/wheelsapp/filesget their paths silently rewritten.Root cause
vendor/wheels/controller/miscellaneous.cfc~254-290: the#873"DOES NOT CONTAIN root → ExpandPath" branch plus thefindNoCase("/wheels", fullPath)substring match, both applied to caller-supplied absolute paths.Proposed direction
When
arguments.directoryis supplied and is an absolute path (e.g.DirectoryExists()or starts with// drive letter), buildfullPath = directory & "/" & fileand skip both the/wheelsmapping rewrite and theExpandPath()fallback. Keep existing behavior for relativefilePath-based resolution. The existing..-traversal guard runs before this block and still applies.Acceptance
sendFile(file=..., directory="/abs/outside/webroot")serves the file on Lucee AND Adobe./wheelsas a substring is not rewritten.file-uploads-and-downloads.mdxline 198 removed once fixed.Reported by the guide-behavioral-audit P1 batch 2 (work item p1-8-uploads, claim C14; live on wheels-test-lucee7:v1.0.0 + wheels-test-adobe2023:v1.0.1).