git-userhub 3.0.1 → 3.0.2
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-user +0 -0
- package/bin/git-user.js +87 -11
- package/package.json +1 -1
- package/test_tar.js +44 -0
package/bin/git-user
CHANGED
|
Binary file
|
package/bin/git-user.js
CHANGED
|
@@ -5,6 +5,7 @@ const https = require('https');
|
|
|
5
5
|
const fs = require('fs');
|
|
6
6
|
const path = require('path');
|
|
7
7
|
const os = require('os');
|
|
8
|
+
const crypto = require('crypto');
|
|
8
9
|
const tar = require('tar');
|
|
9
10
|
const pkg = require('../package.json');
|
|
10
11
|
|
|
@@ -34,25 +35,71 @@ function getPlatform() {
|
|
|
34
35
|
};
|
|
35
36
|
}
|
|
36
37
|
|
|
37
|
-
// Download file
|
|
38
|
-
function
|
|
38
|
+
// Download file to memory (for checksums.txt)
|
|
39
|
+
function fetchText(url, redirectCount = 0) {
|
|
39
40
|
return new Promise((resolve, reject) => {
|
|
41
|
+
if (redirectCount > 3) return reject(new Error('Too many redirects'));
|
|
42
|
+
|
|
43
|
+
const parsedUrl = new URL(url);
|
|
44
|
+
if (parsedUrl.protocol !== 'https:') {
|
|
45
|
+
return reject(new Error('Only HTTPS is allowed'));
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
https.get(url, { headers: { 'User-Agent': 'git-user-cli' } }, (res) => {
|
|
49
|
+
if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
|
|
50
|
+
return fetchText(res.headers.location, redirectCount + 1).then(resolve).catch(reject);
|
|
51
|
+
}
|
|
52
|
+
if (res.statusCode !== 200) {
|
|
53
|
+
return reject(new Error(`Failed to fetch: ${res.statusCode} ${url}`));
|
|
54
|
+
}
|
|
55
|
+
let data = '';
|
|
56
|
+
res.on('data', chunk => data += chunk);
|
|
57
|
+
res.on('end', () => resolve(data));
|
|
58
|
+
}).on('error', reject);
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Download file from URL, and verify hash
|
|
63
|
+
function downloadAndVerify(url, dest, expectedHash, redirectCount = 0) {
|
|
64
|
+
return new Promise((resolve, reject) => {
|
|
65
|
+
if (redirectCount > 3) return reject(new Error('Too many redirects'));
|
|
66
|
+
|
|
67
|
+
const parsedUrl = new URL(url);
|
|
68
|
+
if (parsedUrl.protocol !== 'https:') {
|
|
69
|
+
return reject(new Error('Only HTTPS is allowed'));
|
|
70
|
+
}
|
|
71
|
+
|
|
40
72
|
const file = fs.createWriteStream(dest);
|
|
73
|
+
const hash = crypto.createHash('sha256');
|
|
41
74
|
|
|
42
|
-
https.get(url, (response) => {
|
|
43
|
-
if (response.statusCode
|
|
44
|
-
|
|
75
|
+
https.get(url, { headers: { 'User-Agent': 'git-user-cli' } }, (response) => {
|
|
76
|
+
if (response.statusCode >= 300 && response.statusCode < 400 && response.headers.location) {
|
|
77
|
+
file.close();
|
|
78
|
+
fs.unlink(dest, () => {});
|
|
79
|
+
return downloadAndVerify(response.headers.location, dest, expectedHash, redirectCount + 1)
|
|
80
|
+
.then(resolve)
|
|
81
|
+
.catch(reject);
|
|
45
82
|
}
|
|
46
83
|
if (response.statusCode !== 200) {
|
|
47
|
-
|
|
48
|
-
|
|
84
|
+
file.close();
|
|
85
|
+
fs.unlink(dest, () => {});
|
|
86
|
+
return reject(new Error(`Failed to download: ${response.statusCode} ${url}`));
|
|
49
87
|
}
|
|
88
|
+
|
|
89
|
+
response.on('data', chunk => hash.update(chunk));
|
|
50
90
|
response.pipe(file);
|
|
91
|
+
|
|
51
92
|
file.on('finish', () => {
|
|
52
93
|
file.close();
|
|
94
|
+
const actualHash = hash.digest('hex');
|
|
95
|
+
if (actualHash !== expectedHash) {
|
|
96
|
+
fs.unlink(dest, () => {});
|
|
97
|
+
return reject(new Error(`Checksum mismatch! Expected ${expectedHash}, got ${actualHash}`));
|
|
98
|
+
}
|
|
53
99
|
resolve();
|
|
54
100
|
});
|
|
55
101
|
}).on('error', (err) => {
|
|
102
|
+
file.close();
|
|
56
103
|
fs.unlink(dest, () => {});
|
|
57
104
|
reject(err);
|
|
58
105
|
});
|
|
@@ -71,6 +118,9 @@ function getRelease() {
|
|
|
71
118
|
};
|
|
72
119
|
|
|
73
120
|
https.get(options, (res) => {
|
|
121
|
+
if (res.statusCode !== 200) {
|
|
122
|
+
return reject(new Error(`GitHub API returned ${res.statusCode} for release v${pkg.version}`));
|
|
123
|
+
}
|
|
74
124
|
let data = '';
|
|
75
125
|
res.on('data', (chunk) => data += chunk);
|
|
76
126
|
res.on('end', () => {
|
|
@@ -116,13 +166,39 @@ async function installAndRun() {
|
|
|
116
166
|
console.error(` Looking for a ${osName} binary matching architecture: ${arch}`);
|
|
117
167
|
process.exit(1);
|
|
118
168
|
}
|
|
169
|
+
|
|
170
|
+
console.log('🔐 Fetching checksums...');
|
|
171
|
+
// Fetch checksums.txt from the release assets or release tag
|
|
172
|
+
// GitHub Releases typically attach checksums.txt if goreleaser is used
|
|
173
|
+
const checksumAsset = release.assets?.find(a => a.name === 'checksums.txt');
|
|
174
|
+
let checksumsUrl = '';
|
|
175
|
+
if (checksumAsset) {
|
|
176
|
+
checksumsUrl = checksumAsset.browser_download_url;
|
|
177
|
+
} else {
|
|
178
|
+
// Fallback pattern if asset list doesn't have it, try direct URL
|
|
179
|
+
checksumsUrl = `https://github.com/${REPO}/releases/download/v${pkg.version}/checksums.txt`;
|
|
180
|
+
}
|
|
119
181
|
|
|
120
|
-
|
|
182
|
+
const checksumsText = await fetchText(checksumsUrl);
|
|
183
|
+
|
|
184
|
+
// Parse checksums.txt to find the hash for our asset
|
|
185
|
+
const expectedHashLine = checksumsText.split('\n').find(line => line.includes(asset.name));
|
|
186
|
+
if (!expectedHashLine) {
|
|
187
|
+
throw new Error(`Checksum for ${asset.name} not found in checksums.txt`);
|
|
188
|
+
}
|
|
189
|
+
const expectedHash = expectedHashLine.trim().split(/\s+/)[0];
|
|
190
|
+
|
|
191
|
+
console.log(`⬇️ Downloading ${asset.name} (verifying SHA256 checksum)...`);
|
|
121
192
|
const archivePath = path.join(BIN_DIR, asset.name);
|
|
122
|
-
await
|
|
193
|
+
await downloadAndVerify(asset.browser_download_url, archivePath, expectedHash);
|
|
194
|
+
|
|
195
|
+
console.log('📂 Extracting securely...');
|
|
196
|
+
await tar.extract({
|
|
197
|
+
file: archivePath,
|
|
198
|
+
cwd: BIN_DIR,
|
|
199
|
+
filter: (p) => p === `git-user${ext}` || p === `./git-user${ext}`
|
|
200
|
+
});
|
|
123
201
|
|
|
124
|
-
console.log('📂 Extracting...');
|
|
125
|
-
await tar.extract({ file: archivePath, cwd: BIN_DIR });
|
|
126
202
|
fs.unlinkSync(archivePath);
|
|
127
203
|
|
|
128
204
|
if (fs.existsSync(binaryPath)) {
|
package/package.json
CHANGED
package/test_tar.js
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const crypto = require('crypto');
|
|
3
|
+
const tar = require('tar');
|
|
4
|
+
|
|
5
|
+
const assetName = "git-user_darwin_arm64.tar.gz";
|
|
6
|
+
|
|
7
|
+
async function main() {
|
|
8
|
+
console.log("Creating dummy tar file using node-tar...");
|
|
9
|
+
// create dummy git-user
|
|
10
|
+
fs.writeFileSync('git-user', 'dummy binary');
|
|
11
|
+
fs.writeFileSync('malicious.sh', 'echo bad');
|
|
12
|
+
|
|
13
|
+
await tar.create({
|
|
14
|
+
gzip: true,
|
|
15
|
+
file: assetName,
|
|
16
|
+
}, ['git-user', 'malicious.sh']);
|
|
17
|
+
|
|
18
|
+
console.log("Tar created.");
|
|
19
|
+
|
|
20
|
+
const file = fs.readFileSync(assetName);
|
|
21
|
+
const expectedHash = crypto.createHash('sha256').update(file).digest('hex');
|
|
22
|
+
|
|
23
|
+
fs.unlinkSync('git-user');
|
|
24
|
+
fs.unlinkSync('malicious.sh');
|
|
25
|
+
|
|
26
|
+
console.log("Expected hash:", expectedHash);
|
|
27
|
+
|
|
28
|
+
// Now extract securely
|
|
29
|
+
fs.mkdirSync('bin', { recursive: true });
|
|
30
|
+
|
|
31
|
+
await tar.extract({
|
|
32
|
+
file: assetName,
|
|
33
|
+
cwd: 'bin',
|
|
34
|
+
filter: (p, entry) => {
|
|
35
|
+
console.log('Filtering path:', p, entry.path);
|
|
36
|
+
return p === 'git-user' || p === 'git-user.exe' || p === './git-user' || p === './git-user.exe';
|
|
37
|
+
}
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
const binContents = fs.readdirSync('bin');
|
|
41
|
+
console.log("Bin contents:", binContents);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
main().catch(console.error);
|