afront 1.0.20 → 1.0.21

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/install.js CHANGED
@@ -3,12 +3,13 @@ const fs = require('fs');
3
3
  const path = require('path');
4
4
  const { https } = require('follow-redirects');
5
5
  const { exec } = require('child_process');
6
+ const os = require('os');
6
7
  const tmp = require('tmp');
7
8
  const AdmZip = require('adm-zip');
8
9
  const readline = require('readline');
9
10
 
10
11
  // Configuration
11
- const GITHUB_ZIP_URL = 'https://github.com/Asggen/afront/archive/refs/tags/v1.0.20.zip'; // Updated URL
12
+ const GITHUB_ZIP_URL = 'https://github.com/Asggen/afront/archive/refs/tags/v1.0.21.zip'; // Updated URL
12
13
 
13
14
  // Define files to skip
14
15
  const SKIP_FILES = ['FUNDING.yml', 'CODE_OF_CONDUCT.md', 'SECURITY.md', 'install.js', '.npmrc'];
@@ -69,7 +70,8 @@ const downloadFile = (url, destination) => {
69
70
  });
70
71
  });
71
72
  }).on('error', (err) => {
72
- fs.unlink(destination, () => reject(err));
73
+ // attempt to remove partial download only if it's inside allowed temp locations
74
+ safeUnlink(destination).finally(() => reject(err));
73
75
  });
74
76
  });
75
77
  };
@@ -127,6 +129,97 @@ const createDirIfNotExists = (dirPath) => {
127
129
  });
128
130
  };
129
131
 
132
+ const isPathInside = (root, target) => {
133
+ const r = path.resolve(root);
134
+ const t = path.resolve(target);
135
+ return t === r || t.startsWith(r + path.sep);
136
+ };
137
+
138
+
139
+ // Security: path validated against SAFE_UNLINK_ROOT before unlink
140
+ const SAFE_UNLINK_ROOT = path.resolve(process.cwd());
141
+
142
+ const safeUnlink = async (targetPath) => {
143
+ const resolved = path.resolve(targetPath);
144
+
145
+ if (!resolved.startsWith(SAFE_UNLINK_ROOT + path.sep)) {
146
+ throw new Error(`Blocked unlink outside safe root: ${resolved}`);
147
+ }
148
+
149
+ const stats = await fs.promises.lstat(resolved);
150
+
151
+ if (!stats.isFile()) {
152
+ throw new Error(`Refusing to unlink non-file: ${resolved}`);
153
+ }
154
+
155
+ await fs.promises.unlink(resolved);
156
+ };
157
+
158
+
159
+ const safeRm = (dirPath, cb) => {
160
+ const resolved = path.resolve(dirPath);
161
+ if (!isPathInside(process.cwd(), resolved)) {
162
+ return cb(new Error(`Refusing to remove directory outside current working directory: ${resolved}`));
163
+ }
164
+ fs.rm(resolved, { recursive: true, force: true }, cb);
165
+ };
166
+
167
+ // Perform an atomic-safe move: open source with O_NOFOLLOW, stream to a temp file, fsync, then rename
168
+ const safeMoveFile = async (resolvedSrc, resolvedDest) => {
169
+ const srcFlags = fs.constants.O_RDONLY | fs.constants.O_NOFOLLOW;
170
+ let srcHandle;
171
+ let destHandle;
172
+ const tmpName = `${resolvedDest}.tmp-${process.pid}-${Date.now()}`;
173
+ try {
174
+ srcHandle = await fs.promises.open(resolvedSrc, srcFlags);
175
+ } catch (err) {
176
+ throw err;
177
+ }
178
+
179
+ try {
180
+ const stats = await srcHandle.stat();
181
+ if (stats.isDirectory()) {
182
+ await srcHandle.close();
183
+ throw new Error('Source is a directory');
184
+ }
185
+
186
+ await createDirIfNotExists(path.dirname(resolvedDest));
187
+
188
+ destHandle = await fs.promises.open(tmpName, fs.constants.O_WRONLY | fs.constants.O_CREAT | fs.constants.O_TRUNC, stats.mode);
189
+
190
+ const bufferSize = 64 * 1024;
191
+ const buffer = Buffer.allocUnsafe(bufferSize);
192
+ let position = 0;
193
+ while (true) {
194
+ const { bytesRead } = await srcHandle.read(buffer, 0, bufferSize, position);
195
+ if (!bytesRead) break;
196
+ let written = 0;
197
+ while (written < bytesRead) {
198
+ const { bytesWritten } = await destHandle.write(buffer, written, bytesRead - written);
199
+ written += bytesWritten;
200
+ }
201
+ position += bytesRead;
202
+ }
203
+
204
+ await destHandle.sync();
205
+ await destHandle.close();
206
+ await srcHandle.close();
207
+
208
+ await fs.promises.rename(tmpName, resolvedDest);
209
+ } catch (err) {
210
+ try {
211
+ if (destHandle) await destHandle.close();
212
+ } catch (e) {}
213
+ try {
214
+ await safeUnlink(tmpName, [path.dirname(resolvedDest)]).catch(() => {});
215
+ } catch (e) {}
216
+ try {
217
+ if (srcHandle) await srcHandle.close();
218
+ } catch (e) {}
219
+ throw err;
220
+ }
221
+ };
222
+
130
223
  const promptForFolderName = async () => {
131
224
  const answer = await askQuestion('AFront: Enter the name of the destination folder: ');
132
225
  return answer;
@@ -140,49 +233,94 @@ const promptForReplace = async (dirPath) => {
140
233
  const removeDir = (dirPath) => {
141
234
  return new Promise((resolve, reject) => {
142
235
  console.log(`Removing existing directory: ${dirPath}`);
143
- fs.rm(dirPath, { recursive: true, force: true }, (err) => {
144
- if (err) {
145
- return reject(err);
146
- }
236
+ safeRm(dirPath, (err) => {
237
+ if (err) return reject(err);
147
238
  resolve();
148
239
  });
149
240
  });
150
241
  };
151
242
 
243
+ const SAFE_READDIR_ROOT = path.resolve(os.tmpdir());
244
+
245
+ const safeReaddir = (dirPath, cb) => {
246
+ const resolved = path.resolve(dirPath);
247
+
248
+ if (!resolved.startsWith(SAFE_READDIR_ROOT + path.sep)) {
249
+ return cb(new Error(`Blocked readdir outside safe root: ${resolved}`));
250
+ }
251
+
252
+ fs.lstat(resolved, (err, stats) => {
253
+ if (err) return cb(err);
254
+ if (!stats.isDirectory()) {
255
+ return cb(new Error(`Not a directory: ${resolved}`));
256
+ }
257
+
258
+ fs.readdir(resolved, cb);
259
+ });
260
+ };
261
+
262
+
152
263
  const moveFiles = (srcPath, destPath) => {
153
264
  return new Promise((resolve, reject) => {
154
- fs.readdir(srcPath, (err, files) => {
265
+ safeReaddir(srcPath, (err, files) => {
155
266
  if (err) {
156
267
  return reject(err);
157
268
  }
158
269
  let pending = files.length;
159
270
  if (!pending) return resolve();
160
271
  files.forEach((file) => {
161
- if (SKIP_FILES.includes(file)) {
272
+ // Normalize and validate file name to avoid path traversal
273
+ const fileName = path.basename(file);
274
+ if (SKIP_FILES.includes(fileName)) {
275
+ if (!--pending) resolve();
276
+ return;
277
+ }
278
+
279
+ const srcRoot = path.resolve(srcPath);
280
+ const destRoot = path.resolve(destPath);
281
+ const resolvedSrc = path.resolve(srcPath, file);
282
+ const resolvedDest = path.resolve(destPath, file);
283
+
284
+ // Ensure resolved paths are inside their respective roots
285
+ if (
286
+ !(resolvedSrc === srcRoot || resolvedSrc.startsWith(srcRoot + path.sep)) ||
287
+ !(resolvedDest === destRoot || resolvedDest.startsWith(destRoot + path.sep))
288
+ ) {
289
+ console.warn(`Skipping suspicious path: ${file}`);
162
290
  if (!--pending) resolve();
163
291
  return;
164
292
  }
165
293
 
166
- const srcFile = path.join(srcPath, file);
167
- const destFile = path.join(destPath, file);
168
- fs.stat(srcFile, (err, stats) => {
294
+ // Use lstat to detect symlinks and refuse to follow them
295
+ fs.lstat(resolvedSrc, (err, stats) => {
169
296
  if (err) {
170
297
  return reject(err);
171
298
  }
299
+
300
+ if (stats.isSymbolicLink()) {
301
+ console.warn(`Skipping symbolic link for safety: ${resolvedSrc}`);
302
+ if (!--pending) resolve();
303
+ return;
304
+ }
305
+
172
306
  if (stats.isDirectory()) {
173
- createDirIfNotExists(destFile)
174
- .then(() => moveFiles(srcFile, destFile))
307
+ createDirIfNotExists(resolvedDest)
308
+ .then(() => moveFiles(resolvedSrc, resolvedDest))
175
309
  .then(() => {
176
310
  if (!--pending) resolve();
177
311
  })
178
312
  .catch(reject);
179
313
  } else {
180
- fs.rename(srcFile, destFile, (err) => {
181
- if (err) {
182
- return reject(err);
183
- }
184
- if (!--pending) resolve();
185
- });
314
+ // Use atomic-safe move: copy from a non-following FD to temp file, then rename
315
+ createDirIfNotExists(path.dirname(resolvedDest))
316
+ .then(() => {
317
+ safeMoveFile(resolvedSrc, resolvedDest)
318
+ .then(() => {
319
+ if (!--pending) resolve();
320
+ })
321
+ .catch(reject);
322
+ })
323
+ .catch(reject);
186
324
  }
187
325
  });
188
326
  });
@@ -202,6 +340,29 @@ const main = async () => {
202
340
  folderName = await promptForFolderName();
203
341
  }
204
342
 
343
+ // Sanitize the provided folder name to prevent path traversal or absolute paths
344
+ const sanitizeFolderName = (name) => {
345
+ if (!name) return '';
346
+ // If user provided '.', use current dir basename
347
+ if (name === '.') return path.basename(process.cwd());
348
+ // Disallow absolute paths
349
+ if (path.isAbsolute(name)) {
350
+ console.warn('Absolute paths are not allowed for destination; using basename.');
351
+ name = path.basename(name);
352
+ }
353
+ // Disallow parent traversal and nested paths
354
+ if (name === '..' || name.includes(path.sep) || name.includes('..')) {
355
+ console.warn('Path traversal detected in destination name; using basename of input.');
356
+ name = path.basename(name);
357
+ }
358
+ // Finally, ensure it's a single path segment
359
+ name = path.basename(name);
360
+ if (!name) throw new Error('Invalid destination folder name');
361
+ return name;
362
+ };
363
+
364
+ folderName = sanitizeFolderName(folderName);
365
+
205
366
  const destDir = path.join(process.cwd(), folderName);
206
367
 
207
368
  if (fs.existsSync(destDir)) {
@@ -230,6 +391,15 @@ const main = async () => {
230
391
 
231
392
  await moveFiles(extractedFolderPath, destDir);
232
393
 
394
+ // Remove any bundled lockfiles from the extracted archive to avoid integrity
395
+ // checksum mismatches originating from the release zip's lockfile.
396
+ try {
397
+ await safeUnlink(path.join(destDir, 'package-lock.json')).catch(() => {});
398
+ await safeUnlink(path.join(destDir, 'npm-shrinkwrap.json')).catch(() => {});
399
+ } catch (e) {
400
+ // ignore
401
+ }
402
+
233
403
  await runNpmInstall(destDir);
234
404
 
235
405
  rl.close();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "afront",
3
- "version": "1.0.20",
3
+ "version": "1.0.21",
4
4
  "description": "AFront is a front-end JavaScript library designed to create seamless server-side rendered (SSSR) websites.",
5
5
  "main": "webpack.dev.js",
6
6
  "scripts": {
package/server.js CHANGED
@@ -1,6 +1,7 @@
1
1
  const express = require("express");
2
2
  const path = require("path");
3
3
  const RateLimit = require("express-rate-limit");
4
+ const helmet = require("helmet");
4
5
  require("dotenv").config();
5
6
 
6
7
  const app = express();
@@ -9,6 +10,12 @@ const HOST = process.env.HOST;
9
10
 
10
11
  app.set("trust proxy", 1);
11
12
 
13
+ // Disable the X-Powered-By header to avoid exposing Express
14
+ app.disable("x-powered-by");
15
+
16
+ // Use Helmet to set various HTTP headers for better security
17
+ app.use(helmet());
18
+
12
19
  // Set up rate limiter: maximum of 100 requests per 15 minutes
13
20
  const limiter = RateLimit({
14
21
  windowMs: 15 * 60 * 1000, // 15 minutes
@@ -97,14 +97,14 @@ module.exports = {
97
97
  minifyJS: true,
98
98
  },
99
99
  templateParameters: (compilation, options) => {
100
- const hash = crypto.createHash("sha1");
100
+ const hash = crypto.createHash("sha256");
101
101
  hash.update(compilation.hash);
102
- const sha1Hash = hash.digest("hex");
102
+ const sha256Hash = hash.digest("hex");
103
103
  return {
104
104
  htmlWebpackPlugin: {
105
105
  options: {
106
106
  title: "AFront",
107
- buildTag: `prod-${sha1Hash}`,
107
+ buildTag: `prod-${sha256Hash}`,
108
108
  },
109
109
  },
110
110
  };
@@ -126,4 +126,4 @@ module.exports = {
126
126
  ],
127
127
  }),
128
128
  ],
129
- };
129
+ };
package/webpack.dev.js CHANGED
@@ -95,18 +95,18 @@ module.exports = {
95
95
  filename: "index.html",
96
96
  hash: true, // This will add the hash in the injected scripts
97
97
  templateParameters: (compilation) => {
98
- const hash = crypto.createHash("sha1");
98
+ const hash = crypto.createHash("sha256");
99
99
  hash.update(compilation.hash);
100
- const sha1Hash = hash.digest("hex");
100
+ const sha256Hash = hash.digest("hex");
101
101
  return {
102
102
  htmlWebpackPlugin: {
103
103
  options: {
104
104
  title: "AFront",
105
- buildTag: `dev-${sha1Hash}`, // Set the build tag with the hash for dev
105
+ buildTag: `dev-${sha256Hash}`, // Set the build tag with the hash for dev
106
106
  },
107
107
  },
108
108
  };
109
109
  },
110
110
  }),
111
111
  ],
112
- };
112
+ };
package/webpack.prod.js CHANGED
@@ -101,14 +101,14 @@ module.exports = {
101
101
  minifyJS: true,
102
102
  },
103
103
  templateParameters: (compilation) => {
104
- const hash = crypto.createHash("sha1");
104
+ const hash = crypto.createHash("sha256");
105
105
  hash.update(compilation.hash);
106
- const sha1Hash = hash.digest("hex");
106
+ const sha256Hash = hash.digest("hex");
107
107
  return {
108
108
  htmlWebpackPlugin: {
109
109
  options: {
110
110
  title: "AFront",
111
- buildTag: `prod-${sha1Hash}`,
111
+ buildTag: `prod-${sha256Hash}`,
112
112
  },
113
113
  },
114
114
  };
@@ -133,4 +133,4 @@ module.exports = {
133
133
  performance: {
134
134
  hints: "warning",
135
135
  },
136
- };
136
+ };