-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathsocket-patch
More file actions
executable file
·315 lines (294 loc) · 10.7 KB
/
Copy pathsocket-patch
File metadata and controls
executable file
·315 lines (294 loc) · 10.7 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
#!/usr/bin/env php
<?php
// socket-patch CLI launcher (Composer distribution).
//
// `composer require socketsecurity/socket-patch` puts `socket-patch` on
// `vendor/bin` — useful in PHP/Composer environments where the composer setup
// hook needs the CLI present. On first run this downloads the prebuilt binary
// for the host platform from the matching GitHub release (`v<version>`),
// verifies it against the release's SHA256SUMS, caches it, and execs it. Set
// SOCKET_PATCH_BIN to an existing executable to bypass the download (airgap).
//
// SP_VERSION is a fallback used ONLY when Composer's recorded version for this
// package can't be read (see sp_version); a normal install downloads the binary
// matching the installed package version. Kept in sync by version-sync.sh.
const SP_VERSION = '3.3.0';
const SP_REPO = 'SocketDev/socket-patch';
const SP_BINARY = 'socket-patch';
// Load Composer's autoloader (the bin proxy exposes its path) so we can read the
// version Composer recorded for THIS package — the binary we download must match
// the package the user actually installed, not a constant that could drift.
$sp_autoload = $GLOBALS['_composer_autoload_path'] ?? null;
if (is_string($sp_autoload) && is_file($sp_autoload)) {
require_once $sp_autoload;
}
function sp_fail($msg)
{
fwrite(STDERR, "socket-patch: $msg\n");
exit(1);
}
/**
* The version to fetch. Prefer the version Composer recorded for this package
* (matches what the user installed); fall back to the baked SP_VERSION constant
* (`version-sync.sh` keeps it current) when InstalledVersions is unavailable or
* reports a non-release (dev/branch) version with no matching release binary.
*/
function sp_version()
{
if (class_exists('\\Composer\\InstalledVersions')) {
try {
$v = \Composer\InstalledVersions::getPrettyVersion('socketsecurity/socket-patch');
if ($v !== null) {
$v = ltrim($v, 'v');
if (preg_match('/^\d+\.\d+\.\d+/', $v)) {
return $v;
}
}
} catch (\Throwable $e) {
// fall through to the constant
}
}
return SP_VERSION;
}
/** @return array{0:string,1:string} [target-triple, archive-extension] */
function sp_detect_target()
{
$family = PHP_OS_FAMILY; // 'Darwin' | 'Linux' | 'Windows' | 'BSD' | ...
$machine = strtolower(php_uname('m'));
if (preg_match('/x86_64|amd64|x64/', $machine)) {
$arch = 'x86_64';
} elseif (preg_match('/aarch64|arm64/', $machine)) {
$arch = 'aarch64';
} elseif (preg_match('/i[3-6]86|x86/', $machine)) {
$arch = 'i686';
} elseif (preg_match('/armv7|armhf|arm/', $machine)) {
$arch = 'arm';
} else {
sp_fail("unsupported CPU architecture: $machine");
}
if ($family === 'Darwin') {
if (!in_array($arch, ['x86_64', 'aarch64'], true)) {
sp_fail("unsupported macOS arch: $arch");
}
return ["$arch-apple-darwin", 'tar.gz'];
}
if ($family === 'Windows') {
$map = [
'x86_64' => 'x86_64-pc-windows-msvc',
'aarch64' => 'aarch64-pc-windows-msvc',
'i686' => 'i686-pc-windows-msvc',
];
if (!isset($map[$arch])) {
sp_fail("unsupported Windows arch: $arch");
}
return [$map[$arch], 'zip'];
}
if ($family === 'Linux') {
$libc = sp_is_musl() ? 'musl' : 'gnu';
$suffix = $arch === 'arm' ? 'eabihf' : '';
return ["$arch-unknown-linux-$libc$suffix", 'tar.gz'];
}
sp_fail("unsupported OS: $family");
}
function sp_is_musl()
{
$out = @shell_exec('ldd --version 2>&1');
if ($out && stripos($out, 'musl') !== false) {
return true;
}
return count(glob('/lib/ld-musl-*.so.1') ?: []) > 0;
}
function sp_cache_dir()
{
if (PHP_OS_FAMILY === 'Windows') {
$base = getenv('LOCALAPPDATA') ?: (getenv('USERPROFILE') . '\\AppData\\Local');
} else {
$base = getenv('XDG_CACHE_HOME') ?: (getenv('HOME') . '/.cache');
}
return $base . DIRECTORY_SEPARATOR . 'socket-patch' . DIRECTORY_SEPARATOR . 'bin';
}
/**
* Download `$url`; write to `$dest` if given, else return the body. HTTPS is
* enforced including across redirects: GitHub release downloads redirect to a
* CDN (still HTTPS), but a redirect to http:// would let a network attacker
* serve a malicious binary AND a matching SHA256SUMS (both attacker-controlled),
* defeating the checksum check — so a non-HTTPS URL (initial or redirect target)
* is refused.
*/
function sp_http_get($url, $dest = null)
{
if (stripos($url, 'https://') !== 0) {
sp_fail("refusing non-HTTPS URL: $url");
}
if (function_exists('curl_init')) {
$ch = curl_init($url);
curl_setopt_array($ch, [
CURLOPT_FOLLOWLOCATION => true,
CURLOPT_MAXREDIRS => 10,
// Only fetch/redirect over HTTPS — curl refuses an http:// redirect.
CURLOPT_PROTOCOLS => CURLPROTO_HTTPS,
CURLOPT_REDIR_PROTOCOLS => CURLPROTO_HTTPS,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_FAILONERROR => true,
CURLOPT_USERAGENT => 'socket-patch-composer',
]);
$data = curl_exec($ch);
if ($data === false) {
$err = curl_error($ch);
curl_close($ch);
sp_fail("download failed for $url: $err");
}
curl_close($ch);
} else {
// No curl: follow redirects manually so each hop's scheme can be checked
// (PHP streams can't whitelist redirect protocols). TLS verification is
// on by default; assert it explicitly.
$data = sp_stream_get($url, 10);
}
if ($dest !== null) {
file_put_contents($dest, $data);
return $dest;
}
return $data;
}
/** Manual, HTTPS-only redirect-following GET for the no-curl fallback. */
function sp_stream_get($url, $redirects)
{
if ($redirects < 0) {
sp_fail("too many redirects for $url");
}
if (stripos($url, 'https://') !== 0) {
sp_fail("refusing non-HTTPS URL: $url");
}
$ctx = stream_context_create([
'http' => [
'follow_location' => 0, // we follow manually to vet each hop
'ignore_errors' => true,
'user_agent' => 'socket-patch-composer',
],
'ssl' => ['verify_peer' => true, 'verify_peer_name' => true],
]);
$http_response_header = [];
$data = @file_get_contents($url, false, $ctx);
$status = 0;
$location = null;
foreach ($http_response_header as $h) {
if (preg_match('#^HTTP/\S+\s+(\d+)#', $h, $m)) {
$status = (int) $m[1];
} elseif (stripos($h, 'Location:') === 0) {
$location = trim(substr($h, strlen('Location:')));
}
}
if ($status >= 300 && $status < 400 && $location !== null) {
// GitHub uses absolute HTTPS redirect targets; reject anything else.
return sp_stream_get($location, $redirects - 1);
}
if ($data === false || $status >= 400) {
sp_fail("download failed ($status) for $url");
}
return $data;
}
/** SHA256SUMS lines are "<hex> <filename>" (filename may be `*`-prefixed). */
function sp_verify_sha256($path, $archive, $sums)
{
$expected = null;
foreach (preg_split('/\r?\n/', $sums) as $line) {
$parts = preg_split('/\s+/', trim($line), 2);
if (count($parts) < 2) {
continue;
}
$name = ltrim($parts[1], '*');
if ($name === $archive) {
$expected = $parts[0];
break;
}
}
if ($expected === null) {
sp_fail("no SHA256SUMS entry for $archive");
}
$actual = hash_file('sha256', $path);
if (strcasecmp($actual, $expected) !== 0) {
sp_fail("checksum mismatch for $archive (expected $expected, got $actual)");
}
}
function sp_extract($archivePath, $ext, $dir)
{
// Prefer `tar` (handles tar.gz everywhere; zip via bsdtar on modern
// Windows); fall back to PharData (tar.gz) / ZipArchive (zip).
$cmd = $ext === 'zip'
? sprintf('tar -xf %s -C %s', escapeshellarg($archivePath), escapeshellarg($dir))
: sprintf('tar xzf %s -C %s', escapeshellarg($archivePath), escapeshellarg($dir));
@exec($cmd . ' 2>&1', $out, $code);
if ($code === 0) {
return;
}
if ($ext === 'zip' && class_exists('ZipArchive')) {
$zip = new ZipArchive();
if ($zip->open($archivePath) === true) {
$zip->extractTo($dir);
$zip->close();
return;
}
} elseif (class_exists('PharData')) {
try {
(new PharData($archivePath))->extractTo($dir, null, true);
return;
} catch (Exception $e) {
// fall through
}
}
sp_fail('failed to extract ' . basename($archivePath));
}
function sp_resolve_binary()
{
$env = getenv('SOCKET_PATCH_BIN');
if ($env && is_executable($env)) {
return $env;
}
$ver = sp_version();
list($target, $ext) = sp_detect_target();
$exe = SP_BINARY . (PHP_OS_FAMILY === 'Windows' ? '.exe' : '');
$cached = sp_cache_dir() . DIRECTORY_SEPARATOR . $ver
. DIRECTORY_SEPARATOR . $target . DIRECTORY_SEPARATOR . $exe;
// Cache hit: verified when first downloaded, under the user's own cache dir.
// Trusted without re-verification (re-verifying needs a network fetch each
// run), matching npx / pip / rustup; an attacker able to write here can
// already replace the installed package or the binary itself.
if (is_executable($cached)) {
return $cached;
}
$archive = SP_BINARY . "-$target.$ext";
$base = 'https://github.com/' . SP_REPO . '/releases/download/v' . $ver;
$tmp = sys_get_temp_dir() . DIRECTORY_SEPARATOR . 'socket-patch-' . getmypid();
@mkdir($tmp, 0777, true);
$archivePath = $tmp . DIRECTORY_SEPARATOR . $archive;
sp_http_get("$base/$archive", $archivePath);
$sums = sp_http_get("$base/SHA256SUMS");
sp_verify_sha256($archivePath, $archive, $sums);
sp_extract($archivePath, $ext, $tmp);
$extracted = $tmp . DIRECTORY_SEPARATOR . $exe;
if (!is_file($extracted)) {
sp_fail("release archive $archive did not contain $exe");
}
@mkdir(dirname($cached), 0777, true);
if (!@copy($extracted, $cached)) {
sp_fail("could not cache binary at $cached");
}
if (PHP_OS_FAMILY !== 'Windows') {
@chmod($cached, 0755);
}
return $cached;
}
$bin = sp_resolve_binary();
$args = array_slice($argv, 1);
if (function_exists('pcntl_exec')) {
pcntl_exec($bin, $args);
sp_fail("failed to exec $bin"); // reached only if exec fails
}
$cmd = escapeshellarg($bin);
foreach ($args as $arg) {
$cmd .= ' ' . escapeshellarg($arg);
}
$code = 0;
passthru($cmd, $code);
exit($code);