1
.github/workflows/test.yml
vendored
1
.github/workflows/test.yml
vendored
@@ -57,6 +57,7 @@ jobs:
|
||||
{ name: 'rest', path: 'src/rest/tests', },
|
||||
{ name: 'routing', path: 'tests/routing', },
|
||||
{ name: 'search', path: 'src/search/tests', },
|
||||
{ name: 'shielding', path: 'src/shielding/tests', },
|
||||
context.payload.repository.full_name === 'github/docs-internal' &&
|
||||
{ name: 'translations', path: 'tests/translations', },
|
||||
{ name: 'unit', path: 'tests/unit', },
|
||||
|
||||
@@ -29,6 +29,7 @@ If you are part of an organization that uses SAML single sign-on (SSO), you won
|
||||
Issues, pull requests, and discussions will appear on your contribution graph if they were opened in a standalone repository, not a fork.
|
||||
|
||||
### Commits
|
||||
|
||||
Commits will appear on your contributions graph if they meet **all** of the following conditions:
|
||||
- The email address used for the commits is associated with your account on {% data variables.location.product_location %}.
|
||||
- The commits were made in a standalone repository, not a fork.
|
||||
|
||||
@@ -24,7 +24,7 @@ shortTitle: Merge multiple accounts
|
||||
|
||||
{% else %}
|
||||
|
||||
**Tip:** We recommend using only one personal account to manage both personal and professional repositories.
|
||||
**Tip:** We recommend using only one personal account to manage both personal and professional repositories.
|
||||
|
||||
{% endif %}
|
||||
|
||||
@@ -32,9 +32,10 @@ shortTitle: Merge multiple accounts
|
||||
|
||||
{% warning %}
|
||||
|
||||
**Warning:**
|
||||
**Warning:**
|
||||
- Organization and repository access permissions aren't transferable between accounts. If the account you want to delete has an existing access permission, an organization owner or repository administrator will need to invite the account that you want to keep.
|
||||
- Any commits authored with a GitHub-provided `noreply` email address cannot be transferred from one account to another. If the account you want to delete used the **Keep my email address private** option, it won't be possible to transfer the commits authored by the account you are deleting to the account you want to keep.
|
||||
- Any commits authored with a {% data variables.product.company_short %}-provided `noreply` email address cannot be transferred from one account to another. If the account you want to delete used the **Keep my email address private** option, it won't be possible to transfer the commits authored by the account you are deleting to the account you want to keep.
|
||||
- Issues, pull requests, and discussions will not be attributed to the new account.
|
||||
|
||||
{% endwarning %}
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
**Notes:**{% ifversion ghes or ghae %}
|
||||
- To appear on your profile contributions graph, co-authored commits must meet the same criteria as commits with one author.{% endif %}
|
||||
- When rebasing commits, the original authors of the commit and the person who rebased the commits, whether on the command line or on {% data variables.location.product_location %}, receive contribution credit.
|
||||
- When rebasing commits, the original authors of the commit and the person who rebased the commits, whether on the command line or on {% data variables.location.product_location %}, receive contribution credit.{% ifversion ghec or fpt %}
|
||||
- If you merged multiple personal accounts, issues, pull requests, and discussions will not be attributed to the new account and will not appear on your contribution graph.{% endif %}
|
||||
|
||||
{% endnote %}
|
||||
|
||||
@@ -16,7 +16,6 @@ import {
|
||||
setLanguageFastlySurrogateKey,
|
||||
} from './set-fastly-surrogate-key.js'
|
||||
import handleErrors from '#src/observability/middleware/handle-errors.js'
|
||||
import handleInvalidPaths from '#src/observability/middleware/handle-invalid-paths.js'
|
||||
import handleNextDataPath from './handle-next-data-path.js'
|
||||
import detectLanguage from './detect-language.js'
|
||||
import reloadTree from './reload-tree.js'
|
||||
@@ -66,8 +65,7 @@ import fastlyBehavior from './fastly-behavior.js'
|
||||
import mockVaPortal from './mock-va-portal.js'
|
||||
import dynamicAssets from './dynamic-assets.js'
|
||||
import contextualizeSearch from '#src/search/middleware/contextualize.js'
|
||||
import rateLimit from './rate-limit.js'
|
||||
import handleInvalidQuerystrings from '#src/observability/middleware/handle-invalid-query-strings.js'
|
||||
import shielding from '#src/shielding/middleware/index.js'
|
||||
|
||||
const { DEPLOYMENT_ENV, NODE_ENV } = process.env
|
||||
const isTest = NODE_ENV === 'test' || process.env.GITHUB_ACTIONS === 'true'
|
||||
@@ -201,9 +199,7 @@ export default function (app) {
|
||||
}
|
||||
|
||||
// *** Early exits ***
|
||||
app.use(rateLimit)
|
||||
app.use(instrument(handleInvalidQuerystrings, './handle-invalid-querystrings'))
|
||||
app.use(instrument(handleInvalidPaths, './handle-invalid-paths'))
|
||||
app.use(shielding)
|
||||
app.use(instrument(handleNextDataPath, './handle-next-data-path'))
|
||||
|
||||
// *** Security ***
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import statsd from '../lib/statsd.js'
|
||||
import statsd from '#src/observability/lib/statsd.js'
|
||||
import { noCacheControl, defaultCacheControl } from '../../../middleware/cache-control.js'
|
||||
|
||||
const STATSD_KEY = 'middleware.handle_invalid_querystrings'
|
||||
13
src/shielding/middleware/index.js
Normal file
13
src/shielding/middleware/index.js
Normal file
@@ -0,0 +1,13 @@
|
||||
import express from 'express'
|
||||
|
||||
import handleInvalidQuerystrings from './handle-invalid-query-strings.js'
|
||||
import handleInvalidPaths from './handle-invalid-paths.js'
|
||||
import rateLimit from './rate-limit.js'
|
||||
|
||||
const router = express.Router()
|
||||
|
||||
router.use(rateLimit)
|
||||
router.use(handleInvalidQuerystrings)
|
||||
router.use(handleInvalidPaths)
|
||||
|
||||
export default router
|
||||
@@ -1,7 +1,7 @@
|
||||
import rateLimit from 'express-rate-limit'
|
||||
|
||||
import statsd from '#src/observability/lib/statsd.js'
|
||||
import { noCacheControl } from './cache-control.js'
|
||||
import { noCacheControl } from '../../../middleware/cache-control.js'
|
||||
|
||||
const EXPIRES_IN_AS_SECONDS = 60
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
import { describe } from '@jest/globals'
|
||||
import { get } from '../../../tests/helpers/e2etest.js'
|
||||
|
||||
import { get } from '../helpers/e2etest.js'
|
||||
import {
|
||||
MAX_UNFAMILIAR_KEYS_BAD_REQUEST,
|
||||
MAX_UNFAMILIAR_KEYS_REDIRECT,
|
||||
} from '#src/observability/middleware/handle-invalid-query-strings.js'
|
||||
} from '#src/shielding/middleware/handle-invalid-query-strings.js'
|
||||
|
||||
const alpha = Array.from(Array(26)).map((e, i) => i + 65)
|
||||
const alphabet = alpha.map((x) => String.fromCharCode(x))
|
||||
76
src/shielding/tests/shielding.js
Normal file
76
src/shielding/tests/shielding.js
Normal file
@@ -0,0 +1,76 @@
|
||||
import { get } from '../../../tests/helpers/e2etest.js'
|
||||
|
||||
describe('honeypotting', () => {
|
||||
test('any GET with survey-vote and survey-token query strings is 400', async () => {
|
||||
const res = await get('/en?survey-vote=1&survey-token=2')
|
||||
expect(res.statusCode).toBe(400)
|
||||
expect(res.body).toMatch(/Honeypotted/)
|
||||
expect(res.headers['cache-control']).toMatch('private')
|
||||
})
|
||||
})
|
||||
|
||||
describe('junk paths', () => {
|
||||
test('junk full pathname', async () => {
|
||||
const res = await get('/xmlrpc.php')
|
||||
expect(res.statusCode).toBe(404)
|
||||
expect(res.headers['content-type']).toMatch('text/plain')
|
||||
expect(res.headers['cache-control']).toMatch('public')
|
||||
})
|
||||
|
||||
test('junk base name', async () => {
|
||||
const res = await get('/en/get-started/.env.local')
|
||||
expect(res.statusCode).toBe(404)
|
||||
expect(res.headers['content-type']).toMatch('text/plain')
|
||||
expect(res.headers['cache-control']).toMatch('public')
|
||||
})
|
||||
|
||||
test.each(['/_nextanything', '/_next/data', '/_next/data/'])(
|
||||
'invalid requests for _next prefix %s',
|
||||
async (path) => {
|
||||
const res = await get(path)
|
||||
expect(res.statusCode).toBe(404)
|
||||
expect(res.headers['content-type']).toMatch('text/plain')
|
||||
expect(res.headers['cache-control']).toMatch('public')
|
||||
}
|
||||
)
|
||||
|
||||
test('any URL that ends with /index.md redirects', async () => {
|
||||
const res = await get('/en/get-started/index.md')
|
||||
expect(res.statusCode).toBe(302)
|
||||
expect(res.headers.location).toBe('/en/get-started')
|
||||
})
|
||||
})
|
||||
|
||||
describe('rate limiting', () => {
|
||||
// We can't actually trigger a full rate limit because
|
||||
// then all other tests will all fail. And we can't rely on this
|
||||
// test always being run last.
|
||||
|
||||
test('only happens if you have junk query strings', async () => {
|
||||
const res = await get('/robots.txt?foo=bar')
|
||||
expect(res.statusCode).toBe(200)
|
||||
const limit = parseInt(res.headers['ratelimit-limit'])
|
||||
const remaining = parseInt(res.headers['ratelimit-remaining'])
|
||||
expect(limit).toBeGreaterThan(0)
|
||||
expect(remaining).toBeLessThan(limit)
|
||||
|
||||
// A second request
|
||||
{
|
||||
const res = await get('/robots.txt?foo=buzz')
|
||||
expect(res.statusCode).toBe(200)
|
||||
const newLimit = parseInt(res.headers['ratelimit-limit'])
|
||||
const newRemaining = parseInt(res.headers['ratelimit-remaining'])
|
||||
expect(newLimit).toBe(limit)
|
||||
// Can't rely on `newRemaining == remaining - 1` because of
|
||||
// concurrency of jest-running.
|
||||
expect(newRemaining).toBeLessThan(remaining)
|
||||
}
|
||||
})
|
||||
|
||||
test('nothing happens if no unrecognized query string', async () => {
|
||||
const res = await get('/robots.txt')
|
||||
expect(res.statusCode).toBe(200)
|
||||
expect(res.headers['ratelimit-limit']).toBeUndefined()
|
||||
expect(res.headers['ratelimit-remaining']).toBeUndefined()
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user