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.
@@ -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
- const resolvedPath = path.resolve(filePath);
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
- if (!resolvedPath.startsWith(resolvedStaticDir + path.sep) && resolvedPath !== resolvedStaticDir) {
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "git-watchtower",
3
- "version": "1.11.2",
3
+ "version": "1.11.4",
4
4
  "description": "Terminal-based Git branch monitor with activity sparklines and optional dev server with live reload",
5
5
  "main": "bin/git-watchtower.js",
6
6
  "bin": {
@@ -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
@@ -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
  };