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 +189 -19
- package/package.json +1 -1
- package/server.js +7 -0
- package/webpack.build-prod.js +4 -4
- package/webpack.dev.js +4 -4
- package/webpack.prod.js +4 -4
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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
167
|
-
|
|
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(
|
|
174
|
-
.then(() => moveFiles(
|
|
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
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
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
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
|
package/webpack.build-prod.js
CHANGED
|
@@ -97,14 +97,14 @@ module.exports = {
|
|
|
97
97
|
minifyJS: true,
|
|
98
98
|
},
|
|
99
99
|
templateParameters: (compilation, options) => {
|
|
100
|
-
const hash = crypto.createHash("
|
|
100
|
+
const hash = crypto.createHash("sha256");
|
|
101
101
|
hash.update(compilation.hash);
|
|
102
|
-
const
|
|
102
|
+
const sha256Hash = hash.digest("hex");
|
|
103
103
|
return {
|
|
104
104
|
htmlWebpackPlugin: {
|
|
105
105
|
options: {
|
|
106
106
|
title: "AFront",
|
|
107
|
-
buildTag: `prod-${
|
|
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("
|
|
98
|
+
const hash = crypto.createHash("sha256");
|
|
99
99
|
hash.update(compilation.hash);
|
|
100
|
-
const
|
|
100
|
+
const sha256Hash = hash.digest("hex");
|
|
101
101
|
return {
|
|
102
102
|
htmlWebpackPlugin: {
|
|
103
103
|
options: {
|
|
104
104
|
title: "AFront",
|
|
105
|
-
buildTag: `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("
|
|
104
|
+
const hash = crypto.createHash("sha256");
|
|
105
105
|
hash.update(compilation.hash);
|
|
106
|
-
const
|
|
106
|
+
const sha256Hash = hash.digest("hex");
|
|
107
107
|
return {
|
|
108
108
|
htmlWebpackPlugin: {
|
|
109
109
|
options: {
|
|
110
110
|
title: "AFront",
|
|
111
|
-
buildTag: `prod-${
|
|
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
|
+
};
|