akanjs 3.0.0-alpha.72 → 3.0.0-alpha.73
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/local/apps/serverLifecycle/serverLifecycle-local.db-shm +0 -0
- package/local/apps/serverLifecycle/serverLifecycle-local_solid.db-shm +0 -0
- package/package.json +1 -1
- package/server/akanApp.ts +3 -19
- package/server/imageOptimizer.ts +2 -17
- package/server/staticPath.ts +7 -12
- package/types/server/staticPath.d.ts +7 -3
|
Binary file
|
|
Binary file
|
package/package.json
CHANGED
package/server/akanApp.ts
CHANGED
|
@@ -18,6 +18,7 @@ import { LogHub } from "./logging/logHub";
|
|
|
18
18
|
import { LogStreamRoute } from "./logging/logStreamRoute";
|
|
19
19
|
import { RotatingLogWriter } from "./logging/rotatingLogWriter";
|
|
20
20
|
import { ProcessMetricsCollector } from "./processMetricsCollector";
|
|
21
|
+
import { resolveStaticPath } from "./staticPath";
|
|
21
22
|
import { getWebConfigFromEnv } from "./types";
|
|
22
23
|
|
|
23
24
|
interface ChildState {
|
|
@@ -669,7 +670,7 @@ export class AkanApp {
|
|
|
669
670
|
if (!this.#web.ssr) return null;
|
|
670
671
|
const clientPrefix = "/_akan/client/";
|
|
671
672
|
if (url.pathname.startsWith(clientPrefix)) {
|
|
672
|
-
const filePath =
|
|
673
|
+
const filePath = resolveStaticPath(
|
|
673
674
|
path.join(this.#artifactDir, "client"),
|
|
674
675
|
url.pathname.slice(clientPrefix.length),
|
|
675
676
|
);
|
|
@@ -684,7 +685,7 @@ export class AkanApp {
|
|
|
684
685
|
|
|
685
686
|
for (const prefix of ["/_akan/styles/", "/_akan/fonts/"]) {
|
|
686
687
|
if (!url.pathname.startsWith(prefix)) continue;
|
|
687
|
-
const filePath =
|
|
688
|
+
const filePath = resolveStaticPath(this.#artifactDir, url.pathname.slice("/_akan/".length));
|
|
688
689
|
if (!filePath) return new Response("Not Found", { status: 404 });
|
|
689
690
|
const file = Bun.file(filePath);
|
|
690
691
|
if (!(await file.exists())) return new Response("Not Found", { status: 404 });
|
|
@@ -1018,23 +1019,6 @@ export class AkanApp {
|
|
|
1018
1019
|
return new Response(Bun.file(filePath).stream(), { headers });
|
|
1019
1020
|
}
|
|
1020
1021
|
|
|
1021
|
-
#safeResolve(baseDir: string, urlPath: string): string | null {
|
|
1022
|
-
let decoded: string;
|
|
1023
|
-
try {
|
|
1024
|
-
decoded = decodeURIComponent(urlPath);
|
|
1025
|
-
} catch {
|
|
1026
|
-
return null;
|
|
1027
|
-
}
|
|
1028
|
-
if (decoded.includes("\0")) return null;
|
|
1029
|
-
const normalizedBase = path.resolve(baseDir);
|
|
1030
|
-
const rel = decoded.replace(/^[/\\]+/, "");
|
|
1031
|
-
const resolved = path.resolve(normalizedBase, rel);
|
|
1032
|
-
if (resolved === normalizedBase) return resolved;
|
|
1033
|
-
const baseWithSep = normalizedBase.endsWith(path.sep) ? normalizedBase : `${normalizedBase}${path.sep}`;
|
|
1034
|
-
if (!resolved.startsWith(baseWithSep)) return null;
|
|
1035
|
-
return resolved;
|
|
1036
|
-
}
|
|
1037
|
-
|
|
1038
1022
|
#makeProxyHeaders(req: Request, childIdx: number, server?: Bun.Server<GatewayWsData>) {
|
|
1039
1023
|
return makeAkanChildProxyHeaders(req, childIdx, server?.requestIP(req));
|
|
1040
1024
|
}
|
package/server/imageOptimizer.ts
CHANGED
|
@@ -5,6 +5,7 @@ import { Logger } from "akanjs/common";
|
|
|
5
5
|
import { isAnimatedImage } from "./animatedImage";
|
|
6
6
|
import { ImageOptimizerError } from "./imageOptimizerError";
|
|
7
7
|
import { Semaphore } from "./semaphore";
|
|
8
|
+
import { resolveStaticPath } from "./staticPath";
|
|
8
9
|
import {
|
|
9
10
|
type AkanImageConfig,
|
|
10
11
|
type AkanImageFormat,
|
|
@@ -260,7 +261,7 @@ export class ImageOptimizer {
|
|
|
260
261
|
|
|
261
262
|
async #readLocalImage(href: string): Promise<ImageSource> {
|
|
262
263
|
const url = new URL(href, "http://local.akan");
|
|
263
|
-
const filePath =
|
|
264
|
+
const filePath = resolveStaticPath(this.#publicDir, url.pathname);
|
|
264
265
|
if (!filePath) throw new ImageOptimizerError(400, '"url" parameter is not allowed');
|
|
265
266
|
const file = Bun.file(filePath);
|
|
266
267
|
if (!(await file.exists())) throw new ImageOptimizerError(404, "Image not found");
|
|
@@ -455,22 +456,6 @@ export class ImageOptimizer {
|
|
|
455
456
|
}
|
|
456
457
|
}
|
|
457
458
|
|
|
458
|
-
static #safeResolve(baseDir: string, urlPath: string): string | null {
|
|
459
|
-
let decoded: string;
|
|
460
|
-
try {
|
|
461
|
-
decoded = decodeURIComponent(urlPath);
|
|
462
|
-
} catch {
|
|
463
|
-
return null;
|
|
464
|
-
}
|
|
465
|
-
if (decoded.includes("\0")) return null;
|
|
466
|
-
const normalizedBase = path.resolve(baseDir);
|
|
467
|
-
const rel = decoded.replace(/^[/\\]+/, "");
|
|
468
|
-
const resolved = path.resolve(normalizedBase, rel);
|
|
469
|
-
if (resolved === normalizedBase) return resolved;
|
|
470
|
-
const baseWithSep = normalizedBase.endsWith(path.sep) ? normalizedBase : normalizedBase + path.sep;
|
|
471
|
-
return resolved.startsWith(baseWithSep) ? resolved : null;
|
|
472
|
-
}
|
|
473
|
-
|
|
474
459
|
static async #readResponseBuffer(res: Response, maxBytes: number): Promise<Buffer> {
|
|
475
460
|
const contentLength = Number.parseInt(res.headers.get("content-length") ?? "0", 10);
|
|
476
461
|
if (contentLength > maxBytes) throw new ImageOptimizerError(413, "Remote image is too large");
|
package/server/staticPath.ts
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import fs from "node:fs";
|
|
2
1
|
import path from "node:path";
|
|
3
2
|
|
|
4
3
|
/**
|
|
@@ -9,9 +8,13 @@ import path from "node:path";
|
|
|
9
8
|
* then a NUL check (a truncating byte in a path), then `path.resolve` to collapse the traversal, then
|
|
10
9
|
* containment against the base **with a separator** — without it `/public-secrets` passes a `/public` check.
|
|
11
10
|
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
11
|
+
* Symlinks are followed, like nginx and `serve-static` do: `akan sync` builds `public/libs/<lib>` as a link to
|
|
12
|
+
* `<workspaceRoot>/libs/<lib>/public`, so a link out of the tree is how a lib ships its assets, not an attack.
|
|
13
|
+
* Refusing those would 404 every lib asset under `akan start` and nothing else — `akan build` copies `public/`
|
|
14
|
+
* with `dereference: true`, so a built app has no links left to refuse. Traversal is what the string checks
|
|
15
|
+
* above stop, and they run before any of this touches the filesystem.
|
|
16
|
+
*
|
|
17
|
+
* Callers check existence after this, so a path that resolves to nothing is handed back and 404s there.
|
|
15
18
|
*/
|
|
16
19
|
export const resolveStaticPath = (baseDir: string, urlPath: string): string | null => {
|
|
17
20
|
let decoded: string;
|
|
@@ -27,13 +30,5 @@ export const resolveStaticPath = (baseDir: string, urlPath: string): string | nu
|
|
|
27
30
|
if (resolved === normalizedBase) return resolved;
|
|
28
31
|
const baseWithSep = normalizedBase.endsWith(path.sep) ? normalizedBase : normalizedBase + path.sep;
|
|
29
32
|
if (!resolved.startsWith(baseWithSep)) return null;
|
|
30
|
-
try {
|
|
31
|
-
|
|
32
|
-
const realBase = fs.realpathSync(normalizedBase);
|
|
33
|
-
const realBaseWithSep = realBase.endsWith(path.sep) ? realBase : realBase + path.sep;
|
|
34
|
-
const real = fs.realpathSync(resolved);
|
|
35
|
-
if (real !== realBase && !real.startsWith(realBaseWithSep)) return null;
|
|
36
|
-
} catch {
|
|
37
|
-
}
|
|
38
33
|
return resolved;
|
|
39
34
|
};
|
|
@@ -6,8 +6,12 @@
|
|
|
6
6
|
* then a NUL check (a truncating byte in a path), then `path.resolve` to collapse the traversal, then
|
|
7
7
|
* containment against the base **with a separator** — without it `/public-secrets` passes a `/public` check.
|
|
8
8
|
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
9
|
+
* Symlinks are followed, like nginx and `serve-static` do: `akan sync` builds `public/libs/<lib>` as a link to
|
|
10
|
+
* `<workspaceRoot>/libs/<lib>/public`, so a link out of the tree is how a lib ships its assets, not an attack.
|
|
11
|
+
* Refusing those would 404 every lib asset under `akan start` and nothing else — `akan build` copies `public/`
|
|
12
|
+
* with `dereference: true`, so a built app has no links left to refuse. Traversal is what the string checks
|
|
13
|
+
* above stop, and they run before any of this touches the filesystem.
|
|
14
|
+
*
|
|
15
|
+
* Callers check existence after this, so a path that resolves to nothing is handed back and 404s there.
|
|
12
16
|
*/
|
|
13
17
|
export declare const resolveStaticPath: (baseDir: string, urlPath: string) => string | null;
|