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('