@bakery-framework/core 2.0.0-alpha.7 → 2.0.0-alpha.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bakery-framework/core",
3
- "version": "2.0.0-alpha.7",
3
+ "version": "2.0.0-alpha.8",
4
4
  "description": "Bakery framework core: handlers, router, config, session, caches, logger, compiler.",
5
5
  "keywords": [
6
6
  "bakery",
@@ -21,15 +21,37 @@ export type RouteScanOptions = {
21
21
  const catchAllGlob = (ext: string) => new Bun.Glob(`[[]...*${ext || '.*'}`)
22
22
 
23
23
  // The single-param route forms, in the order they are tried: `[name].ext`
24
- // first, then the escaped-literal `*.ext`. Built here rather than twice inside
24
+ // first, then the literal-asterisk `*.ext`. Built here rather than twice inside
25
25
  // `routeGlobs` — the `dynamicOnly` branch and the combined branch returned
26
26
  // character-identical pairs, and the two must stay in step or a route form
27
27
  // resolves under one caller and not the other. A function, not a hoisted
28
28
  // constant: `ext` varies per handler, and `staticOnly` returns before it needs
29
29
  // them at all.
30
- const dynamicGlobs = (ext: string) => [
30
+ //
31
+ // **The literal asterisk is a character class, `[*]`, never the escape `\*`.**
32
+ // A backslash is the obvious spelling and is unusable on Windows, where `\` is
33
+ // a path separator: Bun read `\*.*` as a drive-absolute pattern, ignored the
34
+ // `cwd` in `GETFILE` entirely, and matched files at `C:\`. A route lookup under
35
+ // a serve root six levels down returned `C:\$WINRE_BACKUP_PARTITION.MARKER`,
36
+ // and `fs.isForbidden` — whose walk is bounded by `startsWith(root)` — waved it
37
+ // through because an out-of-root path skipped the loop and answered "allowed".
38
+ // That clamp now fails closed, so this is belt and braces; both halves are
39
+ // pinned, and neither test can see the other's bug.
40
+ //
41
+ // Measured with a scan whose cwd was a temp directory holding one file: `[*]`
42
+ // yields nothing, `\*` yields four files from the drive root. The escape is
43
+ // also unreachable on Windows in the direction it was meant for — `*` is a
44
+ // reserved character in a Windows filename, so a route file literally named
45
+ // `*.ts` can only exist on POSIX, where both spellings match it identically
46
+ // (verified on Linux). The character class costs nothing and means the same
47
+ // thing on both platforms.
48
+ //
49
+ // Exported only as a test seam: with the clamp in place no test driving
50
+ // `getRoute` can tell the two spellings apart — verified by reverting this line
51
+ // and watching all 17 pass — so the pattern has to be asserted directly.
52
+ export const dynamicGlobs = (ext: string) => [
31
53
  new Bun.Glob(`[[][!.]*${ext || '.*'}`),
32
- new Bun.Glob(`\\*${ext || '.*'}`),
54
+ new Bun.Glob(`[*]${ext || '.*'}`),
33
55
  ]
34
56
 
35
57
  const routeGlobs = (
package/src/utils/fs.ts CHANGED
@@ -395,6 +395,35 @@ export namespace FileSystem {
395
395
  let curr = safeResolve(pathToCheck)
396
396
  const normalizedRoot = safeResolve(root)
397
397
 
398
+ // A path outside `root` is forbidden, and this is the clause that says so.
399
+ //
400
+ // The walk below is bounded by `curr.startsWith(normalizedRoot)`, so an
401
+ // out-of-root path skipped the loop body entirely and fell through to
402
+ // `return false` — "not forbidden". That is a guard failing *open* on the
403
+ // one input it most needs to refuse, and it made every caller that relied
404
+ // on this for containment (all of them: `constants.ts` said so in as many
405
+ // words) incapable of catching an escape.
406
+ //
407
+ // Not theoretical. `$routing.ts` builds its route globs with `\*` to mean a
408
+ // literal asterisk; on Windows a backslash is a path *separator*, so Bun
409
+ // read the pattern as drive-absolute, ignored `cwd`, and matched files at
410
+ // `C:\`. `getRoute` resolved one, asked this function, was told "allowed",
411
+ // and returned an `Info` pointing six levels above the serve root. The glob
412
+ // is fixed too, but the glob was only how it was reached — a resolved file
413
+ // outside the root has to be refused here whatever produced it.
414
+ //
415
+ // The separator suffix matters: a bare `startsWith` would also accept a
416
+ // sibling directory whose name merely begins with the root's. A root that
417
+ // is already a filesystem root ends in `/` and must not gain a second one —
418
+ // `C://` matches nothing, which would make every path under `C:/` read as
419
+ // an escape. `fs.test.ts` drives both through its root corpus.
420
+ const rootPrefix = normalizedRoot.endsWith('/')
421
+ ? normalizedRoot
422
+ : `${normalizedRoot}/`
423
+ if (curr !== normalizedRoot && !curr.startsWith(rootPrefix)) {
424
+ return true
425
+ }
426
+
398
427
  const store = hostStore.getStore()
399
428
  const seen = store ? (store.forbiddenProbes ??= new Map()) : null
400
429