Skip to content

Commit 45498a8

Browse files
authored
fix: validate locale codes to ensure path segment safety (#4059)
1 parent c1187ef commit 45498a8

3 files changed

Lines changed: 32 additions & 2 deletions

File tree

src/module.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ import { relative } from 'pathe'
1414
import { generateTemplateNuxtI18nOptions } from './template'
1515
import { generateI18nTypes, generateLoaderOptions, simplifyLocaleOptions } from './gen'
1616
import { applyLayerOptions, resolveLayerVueI18nConfigInfo } from './layers'
17-
import { computeLocaleHashes, filterLocales, getLocaleFilePaths, normalizeDomainLocale, resolveLocales } from './utils'
17+
import { computeLocaleHashes, filterLocales, getLocaleFilePaths, normalizeDomainLocale, resolveLocales, validateLocaleCodes } from './utils'
1818
import { isString } from '@intlify/shared'
1919

2020
export * from './types'
@@ -181,6 +181,7 @@ export default defineNuxtModule<NuxtI18nOptions>({
181181
normalizeDomainLocale(isString(x) ? { code: x, language: x } : x),
182182
)
183183
ctx.localeCodes = ctx.normalizedLocales.map(locale => locale.code)
184+
validateLocaleCodes(ctx.localeCodes)
184185
ctx.localeInfo = resolveLocales(nuxt.options.srcDir, ctx.normalizedLocales, nuxt.vfs)
185186

186187
ctx.vueI18nConfigPaths = await resolveLayerVueI18nConfigInfo(ctx)

src/utils.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,19 @@ export function filterLocales(ctx: I18nNuxtContext, nuxt: Nuxt) {
3131
return ctx.options.locales.filter(x => include.includes(isString(x) ? x : x.code)) as string[] | LocaleObject[]
3232
}
3333

34+
// locale codes are used as single URL path segments (route prefixes, messages endpoint), in route names and in cookies (#4036)
35+
const INVALID_LOCALE_CODE_CHAR_RE = /[/\\?#%:\s]/
36+
37+
export function validateLocaleCodes(codes: string[]) {
38+
const invalid = codes.filter(code => !code || INVALID_LOCALE_CODE_CHAR_RE.test(code))
39+
if (invalid.length) {
40+
throw new Error(
41+
`[nuxt-i18n] Invalid locale code${invalid.length > 1 ? 's' : ''}: ${invalid.map(x => JSON.stringify(x)).join(', ')}. `
42+
+ 'Locale codes are used as URL path segments and must not be empty or contain `/ \\ ? # % :` or whitespace.',
43+
)
44+
}
45+
}
46+
3447
/**
3548
* Normalizes the single-domain fields (`domain`/`domainDefault`) into their multi-domain forms
3649
* (`domains`/`defaultForDomains`) so runtime domain resolution only handles one shape,

test/utils.test.ts

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { filterLocales, resolveLocales } from '../src/utils'
1+
import { filterLocales, resolveLocales, validateLocaleCodes } from '../src/utils'
22
import type { LocaleObject, NuxtI18nOptions } from '../src/types'
33
import type { I18nNuxtContext } from '../src/context'
44
import type { Nuxt, NuxtConfigLayer } from '@nuxt/schema'
@@ -62,6 +62,22 @@ describe('filterLocales', () => {
6262
})
6363
})
6464

65+
describe('validateLocaleCodes', () => {
66+
test('accepts path-segment-safe codes', () => {
67+
expect(() => validateLocaleCodes(['en', 'de-AT', 'zh-Hans', 'pt_BR', 'kr.v2'])).not.toThrow()
68+
})
69+
70+
test.each(['at/de', 'at\\de', 'en us', 'en?', 'en#x', 'en%20', 'en:us', ''])('throws for %j', code => {
71+
expect(() => validateLocaleCodes([code])).toThrowError('[nuxt-i18n] Invalid locale code')
72+
})
73+
74+
test('lists all invalid codes', () => {
75+
expect(() => validateLocaleCodes(['en', 'at/de', 'at/en'])).toThrowError(
76+
/Invalid locale codes: "at\/de", "at\/en"/,
77+
)
78+
})
79+
})
80+
6581
test('resolveLocales', async () => {
6682
const locales = [
6783
{

0 commit comments

Comments
 (0)