javascript-solid-server 0.0.166 → 0.0.168

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.
@@ -378,7 +378,8 @@
378
378
  "Bash(terser losos/html.js -m -c)",
379
379
  "Bash(terser losos/store.js -m -c)",
380
380
  "Bash(terser losos/registry.js -m -c)",
381
- "Bash(terser losos/losos.js -m -c)"
381
+ "Bash(terser losos/losos.js -m -c)",
382
+ "Bash(DATA_ROOT=/tmp/whatever node --test test/url.test.js)"
382
383
  ]
383
384
  }
384
385
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "javascript-solid-server",
3
- "version": "0.0.166",
3
+ "version": "0.0.168",
4
4
  "description": "A minimal, fast Solid server",
5
5
  "main": "src/index.js",
6
6
  "type": "module",
@@ -362,7 +362,7 @@ function getErrorPage(statusCode, isAuthenticated, request) {
362
362
  <p class="subtitle">${subtitle}</p>
363
363
 
364
364
  <div class="actions">
365
- ${is401 ? `<a href="https://solidos.solidcommunity.net/?uri=${encodeURIComponent(baseUrl + request.url)}" class="btn btn-primary">
365
+ ${is401 ? `<a href="https://solidos.org/docs/browser/?uri=${encodeURIComponent(baseUrl + request.url)}" class="btn btn-primary">
366
366
  Open in Data Browser
367
367
  </a>` : ''}
368
368
  <a href="${baseUrl}/" class="btn btn-secondary">
package/src/utils/url.js CHANGED
@@ -22,8 +22,11 @@ export function updateDataRoot() {
22
22
  * @throws {Error} - If path traversal is detected
23
23
  */
24
24
  export function urlToPath(urlPath) {
25
- // Normalize: remove leading slash, decode URI
26
- let normalized = urlPath.startsWith('/') ? urlPath.slice(1) : urlPath;
25
+ // Normalize: strip all leading slashes (#131 — `//foo` from bot probes
26
+ // would otherwise leave `/foo`, and path.resolve(root, '/foo') would
27
+ // treat the second arg as absolute, escape dataRoot, and trip the
28
+ // traversal guard with a 500 instead of resolving cleanly to a 404).
29
+ let normalized = urlPath.replace(/^\/+/, '');
27
30
  normalized = decodeURIComponent(normalized);
28
31
 
29
32
  // Security: remove path traversal attempts (multiple passes for ....// bypass)
@@ -54,8 +57,8 @@ export function urlToPath(urlPath) {
54
57
  * @throws {Error} - If path traversal is detected
55
58
  */
56
59
  export function urlToPathWithPod(urlPath, podName) {
57
- // Normalize: remove leading slash, decode URI
58
- let normalized = urlPath.startsWith('/') ? urlPath.slice(1) : urlPath;
60
+ // Normalize: strip all leading slashes (#131 — see urlToPath for context).
61
+ let normalized = urlPath.replace(/^\/+/, '');
59
62
  normalized = decodeURIComponent(normalized);
60
63
 
61
64
  // Security: remove path traversal attempts (multiple passes for ....// bypass)
package/test/url.test.js CHANGED
@@ -5,9 +5,10 @@
5
5
  * Regression guard for #278 (single-user root-pod PUT → ENOTDIR).
6
6
  */
7
7
 
8
- import { describe, it } from 'node:test';
8
+ import { describe, it, before, after } from 'node:test';
9
9
  import assert from 'node:assert';
10
- import { getPodName, getContentType } from '../src/utils/url.js';
10
+ import path from 'path';
11
+ import { getPodName, getContentType, urlToPath, urlToPathWithPod } from '../src/utils/url.js';
11
12
 
12
13
  describe('getPodName', () => {
13
14
  describe('subdomain mode', () => {
@@ -126,3 +127,56 @@ describe('getContentType', () => {
126
127
  });
127
128
  });
128
129
  });
130
+
131
+ describe('urlToPath / urlToPathWithPod (#131 — leading-slash normalization)', () => {
132
+ // Bot probes hammer JSS with `//foo`, `///wp-admin/...`, etc. Without
133
+ // multi-slash stripping these used to escape dataRoot via path.resolve
134
+ // (which treats `/foo` as absolute) and 500 with "Path traversal detected"
135
+ // instead of the expected 404.
136
+
137
+ // Save/restore DATA_ROOT — other test suites mutate process.env.DATA_ROOT
138
+ // (via createServer's root option) and don't always restore it. Pinning
139
+ // to './data' keeps the assertions stable across run order.
140
+ let originalDataRoot;
141
+ before(() => {
142
+ originalDataRoot = process.env.DATA_ROOT;
143
+ delete process.env.DATA_ROOT; // forces getDataRoot() default of './data'
144
+ });
145
+ after(() => {
146
+ if (originalDataRoot === undefined) delete process.env.DATA_ROOT;
147
+ else process.env.DATA_ROOT = originalDataRoot;
148
+ });
149
+
150
+ const dataRoot = path.resolve('./data');
151
+
152
+ describe('urlToPath', () => {
153
+ it('resolves a normal path inside dataRoot', () => {
154
+ assert.strictEqual(urlToPath('/alice/profile/card'), path.join(dataRoot, 'alice/profile/card'));
155
+ });
156
+
157
+ it('handles double leading slash without throwing (#131)', () => {
158
+ assert.strictEqual(urlToPath('//about.php'), path.join(dataRoot, 'about.php'));
159
+ });
160
+
161
+ it('handles many leading slashes (#131)', () => {
162
+ assert.strictEqual(urlToPath('////wp-admin/index.php'), path.join(dataRoot, 'wp-admin/index.php'));
163
+ });
164
+
165
+ it('still rejects real `..` traversal that escapes after normalization', () => {
166
+ // Security must be preserved: `/../etc/passwd` → strip leading slash →
167
+ // `../etc/passwd` → strip `..` → `/etc/passwd` (absolute residue) →
168
+ // path.resolve escapes dataRoot → guard fires.
169
+ assert.throws(() => urlToPath('/../etc/passwd'), /Path traversal/);
170
+ });
171
+ });
172
+
173
+ describe('urlToPathWithPod', () => {
174
+ it('resolves into the pod dir', () => {
175
+ assert.strictEqual(urlToPathWithPod('/profile/card', 'alice'), path.join(dataRoot, 'alice/profile/card'));
176
+ });
177
+
178
+ it('handles double leading slash (#131)', () => {
179
+ assert.strictEqual(urlToPathWithPod('//about.php', 'alice'), path.join(dataRoot, 'alice/about.php'));
180
+ });
181
+ });
182
+ });