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
12 changes: 12 additions & 0 deletions open-sse/utils/bypassHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,18 @@ export function handleBypassRequest(body, model, userAgent = "") {
}
}

// Pattern 5: Quota probe — max_tokens=1 + "quota" keyword (FCC try_quota_mock).
if (!shouldBypass && body.max_tokens === 1) {
const userText = messages
.filter((m) => m.role === "user")
.map((m) => getText(m.content))
.join(" ")
.toLowerCase();
if (userText.includes("quota")) {
shouldBypass = true;
}
}

if (!shouldBypass) return null;

const sourceFormat = detectFormat(body);
Expand Down
87 changes: 87 additions & 0 deletions open-sse/utils/claudeCodeMetaRequests.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
// Tools whose meaningful prefix is two words (verb + subcommand).
const TWO_WORD_TOOLS = new Set([
"git", "npm", "docker", "kubectl", "cargo", "go", "pip", "yarn", "pnpm", "bun",
]);

const ENV_ASSIGNMENT_RE = /^[A-Za-z_][A-Za-z0-9_]*=/;
// Command-injection signals: ; | & ` $( (newline handled separately)
const INJECTION_RE = /[;|&`]|\$\(/;

/** Tokenize a shell command on whitespace, respecting simple single/double quotes. */
function tokenize(command: string): string[] {
const tokens: string[] = [];
let current = "";
let quote: string | null = null;
for (const ch of command) {
if (quote) {
if (ch === quote) quote = null;
else current += ch;
} else if (ch === '"' || ch === "'") {
quote = ch;
} else if (/\s/.test(ch)) {
if (current) { tokens.push(current); current = ""; }
} else {
current += ch;
}
}
if (current) tokens.push(current);
return tokens;
}
Comment on lines +11 to +29

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Limitação na tokenização (escapes de aspas): A função tokenize não lida com caracteres de escape (como \" ou \'). Se um comando contiver aspas escapadas dentro de uma string citada (por exemplo, git commit -m "Fix \"bug\""), a tokenização será quebrada, pois o caractere " escapado fechará prematuramente a string.

Podemos tornar a tokenização muito mais robusta adicionando um controle simples para caracteres escapados por barra invertida (\\).

function tokenize(command: string): string[] {
  const tokens: string[] = [];
  let current = "";
  let quote: string | null = null;
  let escaped = false;
  for (const ch of command) {
    if (escaped) {
      current += ch;
      escaped = false;
    } else if (ch === '\\') {
      escaped = true;
    } else if (quote) {
      if (ch === quote) quote = null;
      else current += ch;
    } else if (ch === '"' || ch === "'") {
      quote = ch;
    } else if (/\\s/.test(ch)) {
      if (current) { tokens.push(current); current = ""; }
    } else {
      current += ch;
    }
  }
  if (current) tokens.push(current);
  return tokens;
}


export function extractCommandPrefix(command: string): string {
if (typeof command !== "string" || !command.trim()) return "";
if (INJECTION_RE.test(command) || command.includes("\n")) {
return "command_injection_detected";
}
Comment on lines +31 to +35

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Falso positivo na detecção de injeção de comando: A expressão regular INJECTION_RE (/[;|&']|\$\(/) é executada diretamente na string bruta do comando antes da tokenização. Isso significa que qualquer caractere como ;, |, & ou ` dentro de uma string entre aspas (por exemplo, em uma mensagem de commit: git commit -m "Fix; update dependencies") disparará incorretamente um falso positivo de injeção de comando, retornando "command_injection_detected".

Embora o helper ainda não esteja integrado ao handler principal, vale a pena considerar essa limitação para evitar que comandos legítimos contendo esses caracteres em strings citadas sejam bloqueados ou falhem no bypass.

let tokens = tokenize(command);
// Strip leading FOO=bar env assignments.
while (tokens.length && ENV_ASSIGNMENT_RE.test(tokens[0])) tokens = tokens.slice(1);
if (!tokens.length) return "";
const head = tokens[0];
// Only form a two-word prefix when the second token is an actual subcommand,
// not a flag. We intentionally do not parse flag-arguments (e.g. `git -C <path>`);
// for those we fall back to the single-word head (conservative, never wrong).
if (TWO_WORD_TOOLS.has(head) && tokens.length > 1 && !tokens[1].startsWith("-")) {
return `${head} ${tokens[1]}`;
}
return head;
}

const READ_COMMANDS = new Set(["cat", "head", "tail", "less", "more", "bat", "type"]);
const LISTING_COMMANDS = new Set(["ls", "dir", "find", "tree"]);
// grep flags that consume the NEXT arg (so it is not a filepath).
const GREP_ARG_FLAGS = new Set(["-e", "-f", "-m", "-A", "-B", "-C"]);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Limitação no parsing de flags do grep: O mapeamento atual em GREP_ARG_FLAGS assume apenas flags curtas e isoladas (ex: -C). Se o usuário utilizar flags combinadas (como grep -inC 2 ou grep -C2) ou flags longas (como grep --context=2), o argumento da flag (ex: 2) não será pulado corretamente. Isso fará com que o argumento ou o próprio padrão de busca sejam incorretamente identificados como caminhos de arquivos.


export function extractFilepathsFromCommand(command: string, _output = ""): string[] {
if (typeof command !== "string" || !command.trim()) return [];
const tokens = tokenize(command);
if (!tokens.length) return [];
const head = tokens[0];

if (LISTING_COMMANDS.has(head)) return [];

if (head === "grep") {
const files: string[] = [];
let patternConsumed = false;
for (let i = 1; i < tokens.length; i++) {
const tok = tokens[i];
if (tok.startsWith("-")) {
if (GREP_ARG_FLAGS.has(tok)) {
i++; // skip this flag's argument
// -e/-f supply the pattern itself, so the positional pattern is already consumed.
if (tok === "-e" || tok === "-f") patternConsumed = true;
}
continue;
}
if (!patternConsumed) { patternConsumed = true; continue; } // first non-flag = pattern
files.push(tok);
}
return files;
}

if (READ_COMMANDS.has(head)) {
return tokens.slice(1).filter((t) => !t.startsWith("-"));
}
Comment on lines +82 to +84

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

Bug de corretude: Ao processar comandos de leitura como head ou tail, se o usuário passar uma flag com argumento separado por espaço (por exemplo, head -n 5 file.txt), o número 5 não começa com - e será incorretamente extraído como um caminho de arquivo (retornando ["5", "file.txt"]).

Podemos corrigir isso iterando pelos tokens e pulando o argumento das flags conhecidas que consomem o próximo parâmetro (como -n, -c, --lines, --bytes).

  if (READ_COMMANDS.has(head)) {
    const files: string[] = [];
    const READ_ARG_FLAGS = new Set(["-n", "-c", "--lines", "--bytes"]);
    for (let i = 1; i < tokens.length; i++) {
      const tok = tokens[i];
      if (tok.startsWith("-")) {
        if (READ_ARG_FLAGS.has(tok)) {
          i++;
        }
        continue;
      }
      files.push(tok);
    }
    return files;
  }


return [];
}
15 changes: 15 additions & 0 deletions tests/unit/bypass-handler.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,3 +83,18 @@ test("handleBypassRequest bypasses single-message count probes", async () => {
assert.equal(payload.usage.total_tokens, 2);
assert.equal(payload.choices[0].finish_reason, "stop");
});

test("bypass returns quota response for max_tokens=1 quota probe", () => {
const body = {
max_tokens: 1,
stream: false,
messages: [{ role: "user", content: "Please check the quota for this key" }],
};
const result = handleBypassRequest(body, "x", "claude-cli/2.1.0");
assert.ok(result?.success, "should bypass quota probe");
});

test("bypass does NOT trigger for normal requests", () => {
const body = { messages: [{ role: "user", content: "write a function" }] };
assert.equal(handleBypassRequest(body, "x", "claude-cli/2.1.0"), null);
});
53 changes: 53 additions & 0 deletions tests/unit/claude-code-meta-requests.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import { test } from "node:test";
import assert from "node:assert/strict";
import { extractCommandPrefix } from "../../open-sse/utils/claudeCodeMetaRequests.ts";
import { extractFilepathsFromCommand } from "../../open-sse/utils/claudeCodeMetaRequests.ts";

test("extractCommandPrefix returns two-word prefix for known multi-verb tools", () => {
assert.equal(extractCommandPrefix("git commit -m 'x'"), "git commit");
assert.equal(extractCommandPrefix("npm install lodash"), "npm install");
assert.equal(extractCommandPrefix("docker build ."), "docker build");
});

test("extractCommandPrefix returns single word for simple commands", () => {
assert.equal(extractCommandPrefix("ls -la"), "ls");
assert.equal(extractCommandPrefix("cat file.txt"), "cat");
});

test("extractCommandPrefix strips leading env assignments", () => {
assert.equal(extractCommandPrefix("FOO=bar npm run build"), "npm run");
});

test("extractCommandPrefix detects command injection", () => {
assert.equal(extractCommandPrefix("ls; rm -rf /"), "command_injection_detected");
assert.equal(extractCommandPrefix("echo $(whoami)"), "command_injection_detected");
assert.equal(extractCommandPrefix("cat `id`"), "command_injection_detected");
});

test("extractFilepathsFromCommand returns read files for read commands", () => {
assert.deepEqual(extractFilepathsFromCommand("cat src/a.ts src/b.ts", ""), ["src/a.ts", "src/b.ts"]);
assert.deepEqual(extractFilepathsFromCommand("head -n5 README.md", ""), ["README.md"]);
});
Comment on lines +27 to +30

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Melhoria nos testes: Recomenda-se adicionar um caso de teste para cobrir comandos de leitura com argumentos de flags separados por espaço (ex: head -n 5 README.md), garantindo que o número 5 não seja extraído como arquivo.

Suggested change
test("extractFilepathsFromCommand returns read files for read commands", () => {
assert.deepEqual(extractFilepathsFromCommand("cat src/a.ts src/b.ts", ""), ["src/a.ts", "src/b.ts"]);
assert.deepEqual(extractFilepathsFromCommand("head -n5 README.md", ""), ["README.md"]);
});
test("extractFilepathsFromCommand returns read files for read commands", () => {
assert.deepEqual(extractFilepathsFromCommand("cat src/a.ts src/b.ts", ""), ["src/a.ts", "src/b.ts"]);
assert.deepEqual(extractFilepathsFromCommand("head -n5 README.md", ""), ["README.md"]);
assert.deepEqual(extractFilepathsFromCommand("head -n 5 README.md", ""), ["README.md"]);
});


test("extractFilepathsFromCommand returns empty for listing commands", () => {
assert.deepEqual(extractFilepathsFromCommand("ls -la src/", ""), []);
assert.deepEqual(extractFilepathsFromCommand("find . -name '*.ts'", ""), []);
});

test("extractFilepathsFromCommand skips the grep pattern arg", () => {
assert.deepEqual(extractFilepathsFromCommand("grep -n foo src/a.ts", ""), ["src/a.ts"]);
});

test("extractCommandPrefix stays conservative when a flag precedes the subcommand", () => {
// -C takes a path arg; we don't parse flag-args, so fall back to the head
// (never emit a wrong two-word prefix like "git -C" or "git /x").
assert.equal(extractCommandPrefix("git -C /x commit"), "git");
});

test("extractFilepathsFromCommand handles grep -e pattern then file", () => {
assert.deepEqual(extractFilepathsFromCommand("grep -e pattern a.ts", ""), ["a.ts"]);
});

test("extractFilepathsFromCommand handles grep -f patternfile then file", () => {
assert.deepEqual(extractFilepathsFromCommand("grep -f pats.txt a.ts", ""), ["a.ts"]);
});
Loading