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.