Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 14 additions & 1 deletion src/__tests__/operator-workflow.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { afterAll, afterEach, describe, expect, it, vi } from "vitest";
import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
import { mkdtempSync, rmSync, symlinkSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { runOperator } from "../operator-cli.js";
Expand Down Expand Up @@ -79,6 +79,19 @@ describe("operator draft workflow commands", () => {
expect(JSON.parse(output.error.mock.calls[0][0])).toMatchObject({ format_version: 1, ok: false, command: "drafts create", code: "invalid_input_file" });
});

it("rejects a symbolic-link Markdown file before loading credentials", async context => {
const target = file("linked-target.md", "Hello");
const path = join(dir, "linked.md");
try { symlinkSync(target, path, "file"); } catch (error) {
if (process.platform === "win32" && (error as NodeJS.ErrnoException).code === "EPERM") { context.skip(); return; }
throw error;
}
const fetch = vi.fn(), load = vi.fn(), output = io(); vi.stubGlobal("fetch", fetch);
expect(await runOperator(["drafts", "create", path, "--title", "T"], load, output)).toBe(2);
expect(load).not.toHaveBeenCalled(); expect(fetch).not.toHaveBeenCalled(); expect(output.out).not.toHaveBeenCalled();
expect(JSON.parse(output.error.mock.calls[0][0])).toMatchObject({ code: "invalid_input_file" });
});

it("admits a 1 MiB file, then applies the converter's own bound before any request", async () => {
const fetch = vi.fn(); vi.stubGlobal("fetch", fetch);
const output = io();
Expand Down
9 changes: 7 additions & 2 deletions src/utils/bounded-file.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,17 @@
import { constants } from "node:fs";
import { open } from "node:fs/promises";
import { lstat, open } from "node:fs/promises";

/** Read a bounded regular UTF-8 file, including a bound if it grows after stat. */
export async function readBoundedFile(path: string, maxBytes: number): Promise<string> {
// lstat rejects symbolic links before opening them. Compare the opened file
// with that entry too, so a replacement between the check and open cannot
// redirect a draft command to a different file.
const entry = await lstat(path);
if (!entry.isFile()) throw new Error(`Expected a regular file no larger than ${maxBytes} bytes.`);
const file = await open(path, constants.O_RDONLY | constants.O_NONBLOCK);
try {
const stat = await file.stat();
if (!stat.isFile() || stat.size > maxBytes) throw new Error(`Expected a regular file no larger than ${maxBytes} bytes.`);
if (!stat.isFile() || stat.size > maxBytes || stat.dev !== entry.dev || stat.ino !== entry.ino) throw new Error(`Expected a regular file no larger than ${maxBytes} bytes.`);
const bytes = Buffer.alloc(maxBytes + 1);
let size = 0;
while (size < bytes.length) {
Expand Down
Loading