@khanhcan148/mk 0.1.14 → 0.1.16
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/README.md +5 -4
- package/package.json +1 -1
- package/src/commands/update.js +436 -397
- package/src/lib/auth.js +3 -1
- package/src/lib/copy.js +331 -271
- package/src/lib/download.js +32 -3
- package/src/lib/releases.js +9 -0
package/src/lib/download.js
CHANGED
|
@@ -17,15 +17,22 @@ const TARBALL_URL = `${GITHUB_API}/repos/${KIT_REPO}/tarball/${KIT_BRANCH}`;
|
|
|
17
17
|
* from crafted tarballs with large size fields. 50 MB is well above any kit file. */
|
|
18
18
|
const MAX_ENTRY_SIZE = 50 * 1024 * 1024; // 52428800 bytes
|
|
19
19
|
|
|
20
|
+
/** Hostnames allowed for kit downloads. Hoisted to module scope to avoid rebuilding on each call. */
|
|
21
|
+
const ALLOWED_HOSTS = new Set(['github.com', 'api.github.com', 'codeload.github.com']);
|
|
22
|
+
|
|
23
|
+
/** HTTP status codes that indicate a redirect. Hoisted to module scope alongside ALLOWED_HOSTS. */
|
|
24
|
+
const REDIRECT_STATUSES = new Set([301, 302, 303, 307, 308]);
|
|
25
|
+
|
|
20
26
|
/**
|
|
21
27
|
* Validate that the download URL's hostname is a GitHub domain.
|
|
22
28
|
* Prevents SSRF: caller-supplied URLs (e.g. tarballUrl from GitHub API JSON) could be
|
|
23
29
|
* redirected to an attacker-controlled host, exfiltrating the Bearer token.
|
|
24
30
|
* Allowed: api.github.com, *.github.com (e.g. codeload.github.com)
|
|
31
|
+
* Exported so that releases.js can validate tarball_url at the source.
|
|
25
32
|
* @param {string} url
|
|
26
33
|
* @throws {Error} if hostname is not a GitHub domain
|
|
27
34
|
*/
|
|
28
|
-
function assertGitHubHostname(url) {
|
|
35
|
+
export function assertGitHubHostname(url) {
|
|
29
36
|
let parsed;
|
|
30
37
|
try {
|
|
31
38
|
parsed = new URL(url);
|
|
@@ -33,7 +40,6 @@ function assertGitHubHostname(url) {
|
|
|
33
40
|
throw new Error(`SSRF guard: invalid URL "${url}"`);
|
|
34
41
|
}
|
|
35
42
|
const { hostname } = parsed;
|
|
36
|
-
const ALLOWED_HOSTS = new Set(['github.com', 'api.github.com', 'codeload.github.com']);
|
|
37
43
|
if (!ALLOWED_HOSTS.has(hostname)) {
|
|
38
44
|
throw new Error(
|
|
39
45
|
`SSRF guard: URL hostname "${hostname}" is not allowed. ` +
|
|
@@ -289,12 +295,35 @@ export async function downloadAndExtractKit(token, opts = {}) {
|
|
|
289
295
|
Authorization: `Bearer ${token}`,
|
|
290
296
|
Accept: 'application/vnd.github.v3+json'
|
|
291
297
|
},
|
|
292
|
-
redirect: '
|
|
298
|
+
// S1: Use redirect: 'manual' to prevent the Authorization header from being forwarded
|
|
299
|
+
// to the redirect target (e.g. codeload.github.com). We handle the redirect manually:
|
|
300
|
+
// validate the Location hostname, then re-fetch without the auth header.
|
|
301
|
+
redirect: 'manual'
|
|
293
302
|
});
|
|
294
303
|
} catch (err) {
|
|
295
304
|
throw new Error(`Network connection failed: ${err.message}`);
|
|
296
305
|
}
|
|
297
306
|
|
|
307
|
+
// Handle 3xx redirects: validate Location hostname, then re-fetch without auth header.
|
|
308
|
+
// This prevents the Bearer token from leaking to third-party servers via a compromised redirect.
|
|
309
|
+
if (REDIRECT_STATUSES.has(res.status)) {
|
|
310
|
+
const location = res.headers.get('location');
|
|
311
|
+
if (!location) {
|
|
312
|
+
throw new Error(`GitHub API redirect (${res.status}) had no Location header`);
|
|
313
|
+
}
|
|
314
|
+
// Validate the redirect target is a GitHub hostname before following
|
|
315
|
+
assertGitHubHostname(location);
|
|
316
|
+
try {
|
|
317
|
+
res = await fetch(location, {
|
|
318
|
+
// No Authorization header on the redirect target — the token is only for api.github.com
|
|
319
|
+
headers: { Accept: 'application/vnd.github.v3+json' },
|
|
320
|
+
redirect: 'follow'
|
|
321
|
+
});
|
|
322
|
+
} catch (err) {
|
|
323
|
+
throw new Error(`Network connection failed on redirect: ${err.message}`);
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
|
|
298
327
|
if (!res.ok) {
|
|
299
328
|
throw new Error(`GitHub API error: ${res.status} ${res.statusText}`);
|
|
300
329
|
}
|
package/src/lib/releases.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import semver from 'semver';
|
|
2
2
|
import { GITHUB_API, KIT_REPO } from './constants.js';
|
|
3
|
+
import { assertGitHubHostname } from './download.js';
|
|
3
4
|
|
|
4
5
|
// ---------------------------------------------------------------------------
|
|
5
6
|
// GitHub Releases API helpers
|
|
@@ -53,6 +54,14 @@ export async function fetchLatestRelease(token) {
|
|
|
53
54
|
|
|
54
55
|
const { tag_name: tag, body = null, tarball_url: tarballUrl } = data;
|
|
55
56
|
|
|
57
|
+
// S3: Validate tarball_url hostname before returning it to callers.
|
|
58
|
+
// A compromised GitHub API response could supply an attacker-controlled URL for SSRF.
|
|
59
|
+
try {
|
|
60
|
+
assertGitHubHostname(tarballUrl);
|
|
61
|
+
} catch {
|
|
62
|
+
return { available: false, reason: 'Tarball URL failed hostname validation' };
|
|
63
|
+
}
|
|
64
|
+
|
|
56
65
|
// Parse version: try clean() first (handles v-prefix), then coerce() as fallback.
|
|
57
66
|
// coerce('release-0.2.0') => '0.2.0', coerce('totally-not-semver') => null
|
|
58
67
|
const version = semver.clean(tag) ?? (semver.coerce(tag) ? semver.coerce(tag).version : null);
|