zerogterm 0.2.0-alpha.2 → 0.3.0-alpha.1

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.
@@ -6,8 +6,13 @@ import { promisify } from 'node:util';
6
6
  const execFileAsync = promisify(execFile);
7
7
  const require = createRequire(import.meta.url);
8
8
  const NAME = /^[a-zA-Z0-9][a-zA-Z0-9_.-]{0,48}$/;
9
- /** host, user@host, host:port, user@host:port — no shell metacharacters. */
10
- const SSH_TARGET = /^(?:([A-Za-z0-9._-]+)@)?([A-Za-z0-9.-]+)(?::(\d{1,5}))?$/;
9
+ /**
10
+ * host, user@host, host:port, user@host:port — no shell metacharacters.
11
+ * Host and user must start alphanumeric: a leading '-' would be parsed by
12
+ * ssh's getopt as an option, and `-Fsome.cfg` can point ssh at an attacker
13
+ * -chosen config file (hence ProxyCommand) without any shell involvement.
14
+ */
15
+ const SSH_TARGET = /^(?:([A-Za-z0-9][A-Za-z0-9._-]*)@)?([A-Za-z0-9][A-Za-z0-9.-]*)(?::(\d{1,5}))?$/;
11
16
  export function validateSessionName(name) {
12
17
  const value = name.trim();
13
18
  if (!NAME.test(value)) {
@@ -32,7 +37,8 @@ export function validateSshTarget(input) {
32
37
  const args = ['-tt'];
33
38
  if (portText)
34
39
  args.push('-p', portText);
35
- args.push(destination);
40
+ // '--' ends option parsing, so the destination can never be read as a flag.
41
+ args.push('--', destination);
36
42
  return { target: value, args };
37
43
  }
38
44
  export function parseWslDistributions(output) {
@@ -198,7 +204,9 @@ export class ScreenService {
198
204
  this.onEvent?.('attached', fallback, true);
199
205
  return { ...fallback };
200
206
  }
201
- const name = id.slice('local:'.length);
207
+ // Session ids cross the IPC boundary from the renderer, so re-validate
208
+ // rather than trusting that createLocal produced this one.
209
+ const name = validateSessionName(id.slice('local:'.length));
202
210
  this.spawnCommand(id, 'screen', ['-x', name], onData, onExit);
203
211
  this.onEvent?.('attached', existing, true);
204
212
  return { ...existing, status: 'connected', persistence: 'screen' };
@@ -226,7 +234,7 @@ export class ScreenService {
226
234
  const fallback = this.fallbackLocalSessions.get(id);
227
235
  if (fallback)
228
236
  return fallback;
229
- const name = id.slice('local:'.length);
237
+ const name = validateSessionName(id.slice('local:'.length));
230
238
  return {
231
239
  id,
232
240
  name,
@@ -2,7 +2,10 @@ import { homedir } from 'node:os';
2
2
  import { readFile } from 'node:fs/promises';
3
3
  const TOKEN = /^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/;
4
4
  const HOST = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,253}$/;
5
- const USER = /^[A-Za-z0-9._-]{1,64}$/;
5
+ // Must start alphanumeric like TOKEN/HOST above: the user is concatenated into
6
+ // `user@host`, so a leading '-' makes the whole destination look like an option
7
+ // to ssh's getopt (`-Fevil.cfg@host` reads an attacker-chosen config file).
8
+ const USER = /^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/;
6
9
  const SCREEN = /^[A-Za-z0-9][A-Za-z0-9_.-]{0,48}$/;
7
10
  function words(value) {
8
11
  const result = [];