mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-10-08 00:19:42 +02:00
fix(routes): canonicalize the workspace before comparing it to a resolved path
validateSessionFilePath realpath-resolves the candidate path but compared it against the raw sessionWorkingDir. When the workspace is itself reached through a symlink the two sides live in different namespaces, so relative() reports a spurious `../` and every file in that workspace is judged an escape — reads and writes in the session are refused wholesale. That is not an exotic setup: os.tmpdir() hands back a symlinked path on macOS (/tmp -> /private/tmp), and symlinked project directories and bind-mounted case paths hit it too. Resolve both sides and compare canonical to canonical. This only makes the comparison honest — it does not widen it. The candidate keeps its own realpath, so a symlink pointing out of the workspace and a ../ traversal are still refused, and a workspace that cannot be resolved now fails closed. Three stubs in file-routes.test.ts used a blanket realpathSync.mockReturnValue(escapeTarget), which answers the same path for the workspace and the candidate; with both sides resolved that makes an escape look contained. They now use the input-aware mockImplementation idiom the rest of that file already uses, so the workspace resolves to itself and only the candidate escapes. Verified they still bite: removing the confinement check turns all of them red. Adds test/route-helpers-symlink-confinement.test.ts, which exercises the function against a real symlinked workspace on disk and pins the negative cases (../ escape, symlink-out, missing file) alongside the fix.
This commit is contained in:
@@ -668,7 +668,9 @@ describe('file-routes', () => {
|
||||
|
||||
it('rejects path traversal attempts', async () => {
|
||||
// realpathSync resolves the symlink to a path outside workingDir
|
||||
mockedRealpathSync.mockReturnValue('/etc/passwd' as never);
|
||||
mockedRealpathSync.mockImplementation(
|
||||
(p: string) => (p === harness.ctx._session.workingDir ? p : '/etc/passwd') as never
|
||||
);
|
||||
|
||||
const res = await harness.app.inject({
|
||||
method: 'GET',
|
||||
@@ -773,7 +775,9 @@ describe('file-routes', () => {
|
||||
});
|
||||
|
||||
it('rejects path traversal in raw file serving', async () => {
|
||||
mockedRealpathSync.mockReturnValue('/etc/shadow' as never);
|
||||
mockedRealpathSync.mockImplementation(
|
||||
(p: string) => (p === harness.ctx._session.workingDir ? p : '/etc/shadow') as never
|
||||
);
|
||||
|
||||
const res = await harness.app.inject({
|
||||
method: 'GET',
|
||||
@@ -868,7 +872,9 @@ describe('file-routes', () => {
|
||||
});
|
||||
|
||||
it('rejects symlink targets that escape the session working directory', async () => {
|
||||
mockedRealpathSync.mockReturnValue('/tmp/outside-workdir/link.log' as never);
|
||||
mockedRealpathSync.mockImplementation(
|
||||
(p: string) => (p === harness.ctx._session.workingDir ? p : '/tmp/outside-workdir/link.log') as never
|
||||
);
|
||||
|
||||
const res = await harness.app.inject({
|
||||
method: 'GET',
|
||||
|
||||
Reference in New Issue
Block a user