git-watchtower 1.11.2 → 1.11.4
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/bin/git-watchtower.js +16 -3
- package/package.json +1 -1
- package/src/git/commands.js +2 -0
- package/src/server/coordinator.js +10 -1
- package/src/server/static.js +0 -24
package/bin/git-watchtower.js
CHANGED
|
@@ -2110,10 +2110,23 @@ function createStaticServer() {
|
|
|
2110
2110
|
pathname = path.normalize(pathname).replace(/^(\.\.[\/\\])+/, '');
|
|
2111
2111
|
let filePath = path.join(STATIC_DIR, pathname);
|
|
2112
2112
|
|
|
2113
|
-
// Security: ensure resolved path stays within STATIC_DIR to prevent path traversal
|
|
2114
|
-
|
|
2113
|
+
// Security: ensure resolved path stays within STATIC_DIR to prevent path traversal.
|
|
2114
|
+
// Use realpath to follow symlinks — without this, a symlink inside STATIC_DIR
|
|
2115
|
+
// pointing outside would bypass the startsWith check.
|
|
2115
2116
|
const resolvedStaticDir = path.resolve(STATIC_DIR);
|
|
2116
|
-
|
|
2117
|
+
let resolvedPath = path.resolve(filePath);
|
|
2118
|
+
try {
|
|
2119
|
+
resolvedPath = fs.realpathSync(resolvedPath);
|
|
2120
|
+
} catch {
|
|
2121
|
+
// File doesn't exist — path.resolve is sufficient since there's no symlink to follow.
|
|
2122
|
+
}
|
|
2123
|
+
let realStaticDir;
|
|
2124
|
+
try {
|
|
2125
|
+
realStaticDir = fs.realpathSync(resolvedStaticDir);
|
|
2126
|
+
} catch {
|
|
2127
|
+
realStaticDir = resolvedStaticDir;
|
|
2128
|
+
}
|
|
2129
|
+
if (!resolvedPath.startsWith(realStaticDir + path.sep) && resolvedPath !== realStaticDir) {
|
|
2117
2130
|
res.writeHead(403, { 'Content-Type': 'text/html' });
|
|
2118
2131
|
res.end('<h1>403 Forbidden</h1>');
|
|
2119
2132
|
addServerLog(`GET ${logPath} → 403 (path traversal blocked)`, true);
|
package/package.json
CHANGED
package/src/git/commands.js
CHANGED
|
@@ -336,6 +336,8 @@ async function getChangedFiles(branchName, baseBranch = 'HEAD', cwd) {
|
|
|
336
336
|
* @returns {{added: number, deleted: number}}
|
|
337
337
|
*/
|
|
338
338
|
function parseDiffStats(diffStatOutput) {
|
|
339
|
+
if (!diffStatOutput) return { added: 0, deleted: 0 };
|
|
340
|
+
|
|
339
341
|
// Parse the summary line: "X files changed, Y insertions(+), Z deletions(-)"
|
|
340
342
|
const match = diffStatOutput.match(/(\d+) insertions?\(\+\).*?(\d+) deletions?\(-\)/);
|
|
341
343
|
if (match) {
|
|
@@ -318,7 +318,15 @@ class Coordinator {
|
|
|
318
318
|
*/
|
|
319
319
|
_handleWorkerMessage(socket, msg, setWorkerId, getWorkerId) {
|
|
320
320
|
switch (msg.type) {
|
|
321
|
-
case 'register':
|
|
321
|
+
case 'register': {
|
|
322
|
+
// Prevent re-registration: a socket that already registered cannot change its ID
|
|
323
|
+
const currentId = getWorkerId();
|
|
324
|
+
if (currentId) break;
|
|
325
|
+
|
|
326
|
+
// Reject if this ID is already claimed by a different socket
|
|
327
|
+
const existingSocket = this.workerSockets.get(msg.id);
|
|
328
|
+
if (existingSocket && existingSocket !== socket) break;
|
|
329
|
+
|
|
322
330
|
setWorkerId(msg.id);
|
|
323
331
|
this.workerSockets.set(msg.id, socket);
|
|
324
332
|
this.projects.set(msg.id, {
|
|
@@ -331,6 +339,7 @@ class Coordinator {
|
|
|
331
339
|
this._sendMessage(socket, { type: 'registered', id: msg.id });
|
|
332
340
|
this._notifyProjectsChanged();
|
|
333
341
|
break;
|
|
342
|
+
}
|
|
334
343
|
|
|
335
344
|
case 'state': {
|
|
336
345
|
// Validate sender — only accept state for the worker's own registered ID
|
package/src/server/static.js
CHANGED
|
@@ -63,33 +63,9 @@ function injectLiveReload(html) {
|
|
|
63
63
|
return html;
|
|
64
64
|
}
|
|
65
65
|
|
|
66
|
-
/**
|
|
67
|
-
* Parse git diff --stat output into { added, deleted } counts.
|
|
68
|
-
* @param {string} diffOutput - Output from `git diff --stat`
|
|
69
|
-
* @returns {{ added: number, deleted: number }}
|
|
70
|
-
*/
|
|
71
|
-
function parseDiffStats(diffOutput) {
|
|
72
|
-
if (!diffOutput) return { added: 0, deleted: 0 };
|
|
73
|
-
|
|
74
|
-
// Parse the summary line: "X files changed, Y insertions(+), Z deletions(-)"
|
|
75
|
-
const match = diffOutput.match(/(\d+) insertions?\(\+\).*?(\d+) deletions?\(-\)/);
|
|
76
|
-
if (match) {
|
|
77
|
-
return { added: parseInt(match[1], 10), deleted: parseInt(match[2], 10) };
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
// Try to match just insertions or just deletions
|
|
81
|
-
const insertMatch = diffOutput.match(/(\d+) insertions?\(\+\)/);
|
|
82
|
-
const deleteMatch = diffOutput.match(/(\d+) deletions?\(-\)/);
|
|
83
|
-
return {
|
|
84
|
-
added: insertMatch ? parseInt(insertMatch[1], 10) : 0,
|
|
85
|
-
deleted: deleteMatch ? parseInt(deleteMatch[1], 10) : 0,
|
|
86
|
-
};
|
|
87
|
-
}
|
|
88
|
-
|
|
89
66
|
module.exports = {
|
|
90
67
|
MIME_TYPES,
|
|
91
68
|
getMimeType,
|
|
92
69
|
LIVE_RELOAD_SCRIPT,
|
|
93
70
|
injectLiveReload,
|
|
94
|
-
parseDiffStats,
|
|
95
71
|
};
|