Skip to content

fix(storage): accept keys whose first segment starts with dots - #17

Merged
kamilmysliwiec merged 2 commits into
nestjs:masterfrom
johnnyhuirilef:fix/dotdot-prefixed-key
Oct 12, 2026
Merged

kamilmysliwiec merged 2 commits into
nestjs:masterfrom
johnnyhuirilef:fix/dotdot-prefixed-key

Conversation

@johnnyhuirilef

Copy link
Copy Markdown
Contributor

PR Checklist

Please check if your PR fulfills the following requirements:

PR Type

What kind of change does this PR introduce?

  • Bugfix
  • Feature
  • Code style update (formatting, local variables)
  • Refactoring (no functional changes, no api changes)
  • Build related changes
  • CI related changes
  • Other... Please describe:

What is the current behavior?

Issue Number: N/A

Hi! 👋 LocalDisk refuses a valid key when its first segment starts with two dots, like ..foo/a.

LocalDisk.inside() (lib/disks/local.disk.ts:361) checks !rel.startsWith('..') on the path of the key's parent directory, relative to the root. For ..foo/a the parent is ..foo, so the check reads it as a path above the root. put, copy and move throw StorageInvalidKeyError ("the key leads outside the disk's root through a link"). exists returns false. get and stat throw not found. delete does nothing. A prefix list returns no entries. InMemoryDisk accepts the same keys.

Repro: https://github.com/johnnyhuirilef/nestjs-repros/tree/main/storage-dotdot-prefixed-key

InMemoryDisk foo/a    put: ok   exists: true   get: hello   list: [foo/a]   delete: ok
InMemoryDisk ..foo/a  put: ok   exists: true   get: hello   list: [..foo/a]   delete: ok
InMemoryDisk ..a      put: ok   exists: true   get: hello   list: [..a]   delete: ok
LocalDisk    foo/a    put: ok   exists: true   get: hello   list: [foo/a]   delete: ok
LocalDisk    ..foo/a  put: StorageInvalidKeyError: Invalid storage key: the key leads outside the disk's root through a link   exists: false   get: StorageFileNotFoundError: No file at "..foo/a"   list: []   delete: ok
LocalDisk    ..a      put: ok   exists: true   get: hello   list: [..a]   delete: ok

LocalDisk root holds a "..foo" directory after the failed put of "..foo/a": true

RESULT: BUG: InMemoryDisk accepts "..foo/a" and LocalDisk refuses it

What is the new behavior?

  • inside() treats a path as outside the root only when it is .. or starts with .. and a path separator.
  • put, get, stat, exists, copy, move, delete and list work for keys like ..foo/a and ...dots/a, as they do on InMemoryDisk.
  • Real escapes stay refused. A key with a .. segment is still invalid. A link that points outside the root, or at the parent of the root, still throws StorageInvalidKeyError. This holds for a link named ..escape too.

Does this PR introduce a breaking change?

  • Yes
  • No

No. The change only accepts keys that were valid but refused.

Other information

The shared contract suite in tests/disks.spec.ts now runs ..a, ..foo/a, ...dots/a and docs/..foo/a through put, get, exists, list, copy, move and delete on every disk. tests/local-disk.spec.ts checks that ..foo/a is stored inside the root and that a link named ..escape stays refused. It also checks a link to the parent of the root and a link to the internal directory.

The new ..foo/a cases fail on LocalDisk without the fix and pass with it.

If you prefer a different approach or smaller tests, I am happy to change it. 🙂

LocalDisk treated a key like `..foo/a` as a path that leaves the root.
The root check used `startsWith('..')` on the real parent directory, so
any directory named `..something` at the top level looked like a parent.
put, copy and move threw StorageInvalidKeyError, and exists, get, stat,
delete and list found nothing.

The check now refuses only `..` and `..` followed by a separator. Links
that point outside the root are still refused.
@micalevisk micalevisk mentioned this pull request Oct 11, 2026
5 of 12 tasks
# Conflicts:
#	tests/local-disk.spec.ts
@kamilmysliwiec
kamilmysliwiec merged commit 6618a02 into nestjs:master Oct 12, 2026
5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants