@lizard-build/cli 4.0.60 → 4.0.62

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.
@@ -11,8 +11,23 @@ import { apiErrorFrom, clientHeaders, getRequestToken, APIError } from "./api.js
11
11
 
12
12
  export const DEFAULT_GIT_URL = "https://git.lizard.build";
13
13
 
14
+ /**
15
+ * Where Lizard Git is. LIZARD_GIT_URL must be https (or http on this machine): the Lizard
16
+ * sign-in goes there with every request.
17
+ */
14
18
  export function gitURL(): string {
15
- return (process.env.LIZARD_GIT_URL || DEFAULT_GIT_URL).replace(/\/+$/, "");
19
+ const u = (process.env.LIZARD_GIT_URL || DEFAULT_GIT_URL).replace(/\/+$/, "");
20
+ let parsed: URL;
21
+ try {
22
+ parsed = new URL(u);
23
+ } catch {
24
+ throw new APIError(400, `LIZARD_GIT_URL is not a URL: ${u}`);
25
+ }
26
+ const local = ["localhost", "127.0.0.1", "[::1]"].includes(parsed.hostname);
27
+ if (parsed.protocol !== "https:" && !(parsed.protocol === "http:" && local)) {
28
+ throw new APIError(400, `LIZARD_GIT_URL must be https:// (your Lizard sign-in is sent there), got ${u}`);
29
+ }
30
+ return u;
16
31
  }
17
32
 
18
33
  /** The credential Lizard Git gets: LIZARD_GIT_TOKEN, else the CLI's sign-in. */
@@ -61,7 +76,12 @@ export function qs(path: string, values: Record<string, string | number | boolea
61
76
  }
62
77
 
63
78
  export const seg = encodeURIComponent;
64
- export const segs = (p: string) => p.split("/").map(seg).join("/");
79
+ /** A branch or file path, each part escaped; . and .. parts refused (they'd reach other URLs). */
80
+ export const segs = (p: string) => {
81
+ const parts = p.split("/");
82
+ if (parts.some((x) => x === "" || x === "." || x === "..")) throw new APIError(400, `bad path: ${p}`);
83
+ return parts.map(seg).join("/");
84
+ };
65
85
 
66
86
  let me: { login: string } | null = null;
67
87
  /** Your Lizard Git login. */
@@ -74,7 +94,7 @@ export async function myLogin(): Promise<string> {
74
94
  export function repoFromRemote(url: string, base = gitURL()): string | null {
75
95
  const host = new URL(base).host;
76
96
  const m = url.trim().match(/^(?:https?:\/\/)(?:[^@/]+@)?([^/]+)\/([^/]+)\/([^/]+?)(?:\.git)?\/?$/);
77
- if (!m || m[1] !== host) return null;
97
+ if (!m || m[1] !== host || [m[2], m[3]].some((x) => x === "." || x === "..")) return null;
78
98
  return `${m[2]}/${m[3]}`;
79
99
  }
80
100
 
@@ -98,8 +118,9 @@ function originRepo(): string | null {
98
118
  export async function resolveRepo(flag?: string): Promise<string> {
99
119
  if (flag) {
100
120
  const f = flag.replace(/\.git$/, "");
101
- if (/^[^/\s]+\/[^/\s]+$/.test(f)) return f;
102
- if (/^[^/\s]+$/.test(f)) return `${await myLogin()}/${f}`;
121
+ const ok = (x: string) => /^[A-Za-z0-9._-]+$/.test(x) && x !== "." && x !== "..";
122
+ if (/^[^/\s]+\/[^/\s]+$/.test(f) && f.split("/").every(ok)) return f;
123
+ if (ok(f)) return `${await myLogin()}/${f}`;
103
124
  throw new APIError(400, `--repo is owner/name or name, got "${flag}"`);
104
125
  }
105
126
  const r = originRepo();
@@ -5,7 +5,7 @@ import { join, dirname } from "node:path";
5
5
  import os from "node:os";
6
6
  import { spawn } from "node:child_process";
7
7
 
8
- export const CURRENT_VERSION = "4.0.60";
8
+ export const CURRENT_VERSION = "4.0.62";
9
9
  const RELEASES_API = "https://api.github.com/repos/lizard-build/lizard-cli/releases/latest";
10
10
  const RELEASE_BASE = "https://github.com/lizard-build/lizard-cli/releases/latest/download";
11
11
 
@@ -1,5 +1,5 @@
1
1
  import { describe, it, expect, afterEach, vi } from "vitest";
2
- import { repoFromRemote, qs, gitURL, helperCommand, resolveRepo, lg } from "../../src/lib/lizard-git.js";
2
+ import { repoFromRemote, qs, gitURL, helperCommand, resolveRepo, lg, segs } from "../../src/lib/lizard-git.js";
3
3
 
4
4
  afterEach(() => {
5
5
  vi.unstubAllGlobals();
@@ -46,3 +46,20 @@ describe("lizard git helpers", () => {
46
46
  await expect(lg.get("/api/repos/a/b")).rejects.toMatchObject({ status: 404, message: "no such repository" });
47
47
  });
48
48
  });
49
+
50
+ describe("lizard git safety", () => {
51
+ it("refuses a LIZARD_GIT_URL that would send the sign-in in clear text", () => {
52
+ vi.stubEnv("LIZARD_GIT_URL", "http://evil.example");
53
+ expect(() => gitURL()).toThrow(/https/);
54
+ vi.stubEnv("LIZARD_GIT_URL", "http://127.0.0.1:8080");
55
+ expect(gitURL()).toBe("http://127.0.0.1:8080");
56
+ });
57
+
58
+ it("refuses . and .. in repository names and paths", async () => {
59
+ await expect(resolveRepo("../x")).rejects.toThrow();
60
+ await expect(resolveRepo("a/..")).rejects.toThrow();
61
+ expect(() => segs("../../api/user/tokens")).toThrow(/bad path/);
62
+ expect(segs("src/a b.js")).toBe("src/a%20b.js");
63
+ expect(repoFromRemote("https://git.lizard.build/../x.git")).toBeNull();
64
+ });
65
+ });