GitHub Copilot and HTTP form injection

GitHub uses this code snippet on its homepage to advertise its AI programming assistant Copilot:

import { fetch } from "fetch-h2";

// Determine whether the sentiment of text is positive
// Use a web service
async function isPositive(text: string): Promise<boolean> {
  const response = await fetch(`http://text-processing.com/api/sentiment/`, {
    method: "POST",
    body: `text=${text}`,
    headers: {
      "Content-Type": "application/x-www-form-urlencoded",
    },
  });
  const json = await response.json();
  return json.label === "pos";
}

At first glance the code works as expected. It calls a webservice that evaluates the sentiment of a text and returns true if that sentiment is positive:

await isPositive("I saw a cat today and it made me happy :D");
// returns true

What if we replace the "and" with an ampersand?

await isPositive("I saw a cat today & it made me happy :D");
// returns false

That's not because the sentence is now less positive, but because the isPositive function turns its input into a request body without escaping. The above function call results in the following HTTP request body:

text=I saw a cat today & it made me happy :D

The content type of this request body is application/x-www-form-urlencoded . This content type consists of keys and values, where an equals sign = separates a key from its value, and an ampersand & separates one key-value-pair from the next. So in this case, the value of the text key is just I saw a cat today , and the it made me happy :D is interpreted as another key with an empty value.

Therefore, the web service only analyzes the text "I saw a cat today" which is neutral, not positive. If we wanted to analyze the entire text, the ampersand should be escaped. Fortunately, that's easy to do:

import { fetch } from "fetch-h2";

// Determine whether the sentiment of text is positive
// Use a web service
async function isPositive(text: string): Promise<boolean> {
  const response = await fetch(`http://text-processing.com/api/sentiment/`, {
    method: "POST",
    body: new URLSearchParams({ text }).toString(),
    headers: {
      "Content-Type": "application/x-www-form-urlencoded",
    },
  });
  const json = await response.json();
  return json.label === "pos";
}

The URLSearchParams API is available in all modern browsers and node.js, and takes care of properly encoding keys and values.

Now:

await isPositive("I saw a cat today & it made me happy :D");
// returns true

Modern browsers and node.js versions also have a built-in fetch function. Unlike the fetch-h2 package, native fetch in node.js only provides HTTP 1.1 connections, not HTTP 2. The native fetch function allows you to pass an instance of URLSearchParams as a value for body, so there's no need to call toString(). This also sets the value of the Content-Type header to application/x-www-form-urlencoded automatically, which simplifies the code to:

// Determine whether the sentiment of text is positive
// Use a web service
async function isPositive(text: string): Promise<boolean> {
  const response = await fetch(`http://text-processing.com/api/sentiment/`, {
    method: "POST",
    body: new URLSearchParams({ text }),
  });
  const json = await response.json();
  return json.label === "pos";
}

It looks like the Copilot doesn't know the intricacies of the application/x-www-form-urlencoded content type. But now you do! If you use an AI assistant for your own work, please remember to review its code carefully. Nobody is perfect and AI isn't perfect either.