diff --git a/content/authentication/keeping-your-account-and-data-secure/managing-your-personal-access-tokens.md b/content/authentication/keeping-your-account-and-data-secure/managing-your-personal-access-tokens.md
index 9cc6c1285c71..ba0b5d0ac34b 100644
--- a/content/authentication/keeping-your-account-and-data-secure/managing-your-personal-access-tokens.md
+++ b/content/authentication/keeping-your-account-and-data-secure/managing-your-personal-access-tokens.md
@@ -144,7 +144,7 @@ Below are some example URLs that generate the tokens we see most often:
* [GitHub Models access](https://github.com/settings/personal-access-tokens/new?name=GitHub+Models+token&description=Used%20to%20call%20GitHub%20Models%20APIs%20to%20easily%20run%20LLMs%3A%20https%3A%2F%2Fdocs.github.com%2Fgithub-models%2Fquickstart%23step-2-make-an-api-call&user_models=read)
* [Update code and open a PR](https://github.com/settings/personal-access-tokens/new?name=Core-loop+token&description=Write%20code%20and%20push%20it%20to%20main%21%20Includes%20permission%20to%20edit%20workflow%20files%20for%20Actions%20-%20remove%20%60workflows%3Awrite%60%20if%20you%20don%27t%20need%20to%20do%20that&contents=write&pull_requests=write&workflows=write)
* [Manage Copilot licenses in an organization](https://github.com/settings/personal-access-tokens/new?name=Core-loop+token&description=Enable%20or%20disable%20copilot%20access%20for%20users%20with%20the%20Seat%20Management%20APIs%3A%20https%3A%2F%2Fdocs.github.com%2Frest%2Fcopilot%2Fcopilot-user-management%0ABe%20sure%20to%20select%20an%20organization%20for%20your%20resource%20owner%20below%21&organization_copilot_seat_management=write)
-* [Make Copilot requests](https://github.com/settings/personal-access-tokens/new?name=Copilot+requests+token&description=Make%20Copilot%20API%20requests%20on%20behalf%20of%20the%20user%2C%20consuming%20premium%20requests%3A%20https%3A%2F%2Fdocs.github.com%2Fcopilot%2Fconcepts%2Fbilling%2Fcopilot-requests&copilot_requests=write)
+* [Make Copilot requests](https://github.com/settings/personal-access-tokens/new?name=Copilot+requests+token&description=Make%20Copilot%20API%20requests%20on%20behalf%20of%20the%20user%2C%20consuming%20premium%20requests%3A%20https%3A%2F%2Fdocs.github.com%2Fcopilot%2Fconcepts%2Fbilling%2Fcopilot-requests&user_copilot_requests=read)
#### Supported Query Parameters
diff --git a/content/copilot/reference/customization-cheat-sheet.md b/content/copilot/reference/customization-cheat-sheet.md
index 90b3985ca5b3..079b0ec0838f 100644
--- a/content/copilot/reference/customization-cheat-sheet.md
+++ b/content/copilot/reference/customization-cheat-sheet.md
@@ -49,7 +49,7 @@ This table shows which customization features are supported in each IDE and surf
* ✗ = not supported
* P = under preview
-| Feature | {% data variables.product.prodname_vscode_shortname %} | {% data variables.product.prodname_vs %} | JetBrains IDEs | Eclipse | Xcode | {% data variables.product.prodname_dotcom_the_website %} | {% data variables.copilot.copilot_cli_short %} |
+| Feature | {% data variables.product.prodname_vscode_shortname %} | {% data variables.product.prodname_vs %} | JetBrains IDEs | Eclipse | Xcode | {% data variables.product.prodname_dotcom %} .com | {% data variables.copilot.copilot_cli_short %} |
|---------|:-------:|:-------------:|:---------:|:-------:|:-----:|:-------:|:---:|
| Custom instructions | ✓ | ✓ | P | P | P | ✓ | ✓ |
| Prompt files | ✓ | ✓ | P | ✗ | P | ✗ | ✓ |
diff --git a/src/shielding/middleware/handle-invalid-paths.ts b/src/shielding/middleware/handle-invalid-paths.ts
index 6f53e9b51f2a..f2def92c8879 100644
--- a/src/shielding/middleware/handle-invalid-paths.ts
+++ b/src/shielding/middleware/handle-invalid-paths.ts
@@ -79,8 +79,7 @@ export default function handleInvalidPaths(
// We can all the CDN to cache these responses because they're
// they're not going to suddenly work in the next deployment.
defaultCacheControl(res)
- res.setHeader('content-type', 'text/plain')
- res.status(404).send('Not found')
+ res.status(404).type('text').send('Not found')
return
}
diff --git a/src/shielding/middleware/handle-invalid-query-string-values.ts b/src/shielding/middleware/handle-invalid-query-string-values.ts
index 3b5b0ee55927..39409d63be86 100644
--- a/src/shielding/middleware/handle-invalid-query-string-values.ts
+++ b/src/shielding/middleware/handle-invalid-query-string-values.ts
@@ -71,9 +71,9 @@ export default function handleInvalidQuerystringValues(
// For example ?foo[bar]=baz (but not ?foo=bar&foo=baz)
if (value instanceof Object && !Array.isArray(value)) {
- const message = `Invalid query string key (${key})`
+ const message = 'Invalid query string'
defaultCacheControl(res)
- res.status(400).send(message)
+ res.status(400).type('text').send(message)
const tags = ['response:400', `path:${req.path}`, `key:${key}`]
statsd.increment(STATSD_KEY, 1, tags)
diff --git a/src/shielding/middleware/handle-invalid-query-strings.ts b/src/shielding/middleware/handle-invalid-query-strings.ts
index 2277225efa60..d53e911a5468 100644
--- a/src/shielding/middleware/handle-invalid-query-strings.ts
+++ b/src/shielding/middleware/handle-invalid-query-strings.ts
@@ -70,8 +70,7 @@ export default function handleInvalidQuerystrings(
if (invalidKeys.length > 0) {
noCacheControl(res)
- const invalidKey = invalidKeys[0].replace(/\[.*$/, '') // Get the base key name
- res.status(400).send(`Invalid query string key (${invalidKey})`)
+ res.status(400).type('text').send('Invalid query string')
const tags = [
'response:400',
@@ -105,7 +104,7 @@ export default function handleInvalidQuerystrings(
noCacheControl(res)
const message = honeypotted ? 'Honeypotted' : 'Too many unrecognized query string parameters'
- res.status(400).send(message)
+ res.status(400).type('text').send(message)
const tags = [
'response:400',
diff --git a/src/shielding/middleware/handle-malformed-urls.ts b/src/shielding/middleware/handle-malformed-urls.ts
index a785864e5069..f4f13e75c4b3 100644
--- a/src/shielding/middleware/handle-malformed-urls.ts
+++ b/src/shielding/middleware/handle-malformed-urls.ts
@@ -22,8 +22,7 @@ export default function handleMalformedUrls(
} catch {
// If any decoding fails, this is a malformed URL
defaultCacheControl(res)
- res.setHeader('content-type', 'text/plain')
- res.status(400).send('Bad Request: Malformed URL')
+ res.status(400).type('text').send('Bad Request: Malformed URL')
return
}
diff --git a/src/shielding/tests/invalid-querystrings.ts b/src/shielding/tests/invalid-querystrings.ts
index 82eb21230b3c..2e4ef943c523 100644
--- a/src/shielding/tests/invalid-querystrings.ts
+++ b/src/shielding/tests/invalid-querystrings.ts
@@ -21,6 +21,7 @@ describe('invalid query strings', () => {
const url = `/?${sp}`
const res = await get(url)
expect(res.statusCode).toBe(400)
+ expect(res.headers['content-type']).toMatch('text/plain')
expect(res.headers['cache-control']).toMatch('no-store')
expect(res.headers['cache-control']).toMatch('private')
})
@@ -69,14 +70,20 @@ describe('invalid query strings', () => {
const url = `/en?query[foo]=bar`
const res = await get(url)
expect(res.statusCode).toBe(400)
- expect(res.body).toMatch('Invalid query string key (query)')
+ expect(res.headers['content-type']).toMatch('text/plain')
+ expect(res.body).toMatch('Invalid query string')
+ // Must not reflect the user-supplied key name
+ expect(res.body).not.toContain('(query)')
})
test('query string keys with square brackets', async () => {
const url = `/?constructor[foo][bar]=buz`
const res = await get(url)
expect(res.statusCode).toBe(400)
- expect(res.body).toMatch('Invalid query string key (constructor)')
+ expect(res.headers['content-type']).toMatch('text/plain')
+ expect(res.body).toMatch('Invalid query string')
+ // Must not reflect the user-supplied key name
+ expect(res.body).not.toContain('(constructor)')
})
test('bad tool query string with Chinese URL-encoded characters', async () => {
@@ -86,6 +93,14 @@ describe('invalid query strings', () => {
expect(res.statusCode).toBe(302)
expect(res.headers.location).toBe('/?tool=azure_data_studio')
})
+
+ test('XSS payloads in bracket query keys are not reflected', async () => {
+ const res = await get('/en?%3Cscript%3Ealert()%3C/script%3E[]')
+ expect(res.statusCode).toBe(400)
+ expect(res.headers['content-type']).toMatch('text/plain')
+ expect(res.body).not.toContain('