CVE-2026-40255
Description
AdonisJS HTTP Server is a package for handling HTTP requests in the AdonisJS framework. In @adonisjs/http-server versions prior to 7.8.1 and 8.0.0-next.0 through 8.1.3, and @adonisjs/core versions prior to 7.4.0, the response.redirect().back() method reads the Referer header from the incoming HTTP request and redirects to that URL without validating the host.An attacker who can influence the Referer header can cause the application to redirect users to a malicious external site. This affects all AdonisJS applications that use response.redirect().back() or response.redirect('back'). This issue has been fixed in versions 7.8.1 and 8.2.0 and 7.4.0 of @adonisjs/core.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
@adonisjs/http-servernpm | >= 8.0.0-next.0, < 8.2.0 | 8.2.0 |
@adonisjs/corenpm | < 7.3.1 | 7.3.1 |
@adonisjs/http-servernpm | < 7.8.1 | 7.8.1 |
Affected products
2Patches
12008fb6cf4f6feat: add isValidRedirectUrl helper, ctx on Redirect, and helper tests
5 files changed · +212 −16
src/helpers.ts+64 −16 modified@@ -31,14 +31,71 @@ import { export { createURL } +/** + * Validates that a URL is safe to use as a redirect destination. + * + * - Relative URLs must start with `/` and not be protocol-relative (`//`) + * - Absolute URLs must parse successfully and their host must match + * `currentHost` or be listed in `allowedHosts` + * + * When `currentHost` and `allowedHosts` are omitted, absolute URLs + * are accepted as long as they parse successfully. + * + * @param url - The URL to validate + * @param currentHost - The current request's Host header value + * @param allowedHosts - Array of additionally allowed hosts + */ +export function isValidRedirectUrl( + url: string, + currentHost?: string, + allowedHosts?: string[] +): boolean { + if (typeof url !== 'string' || url.trim() === '') { + return false + } + + if (url.startsWith('//')) { + return false + } + + if (url.startsWith('/')) { + try { + const parsed = new URL(url, 'http://localhost') + return parsed.host === 'localhost' + } catch { + return false + } + } + + try { + const parsed = new URL(url) + + /** + * When no host constraints are provided, accept any + * parseable absolute URL + */ + if (!currentHost && (!allowedHosts || allowedHosts.length === 0)) { + return true + } + + if (currentHost && parsed.host === currentHost) { + return true + } + + if (allowedHosts && allowedHosts.length > 0 && allowedHosts.includes(parsed.host)) { + return true + } + + return false + } catch { + return false + } +} + /** * Returns the previous URL from the request's `Referer` header, * validated against the request's `Host` header and an optional - * list of allowed hosts. - * - * The referrer is accepted when its host matches the request's - * `Host` header or is listed in `allowedHosts`. Otherwise the - * `fallback` value is returned. + * list of allowed hosts using `isValidRedirectUrl`. * * @param headers - The incoming request headers * @param allowedHosts - Array of allowed referrer hosts @@ -58,17 +115,8 @@ export function getPreviousUrl( referrer = referrer[0] } - try { - const parsed = new URL(referrer) - const host = headers['host'] - if (host && parsed.host === host) { - return referrer - } - if (allowedHosts.length > 0 && allowedHosts.includes(parsed.host)) { - return referrer - } - } catch { - // malformed URL + if (isValidRedirectUrl(referrer, headers['host'], allowedHosts)) { + return referrer } return fallback
src/redirect.ts+7 −0 modified@@ -24,6 +24,7 @@ import type { } from './types/url_builder.ts' import { safeDecodeURI } from './utils.ts' import Macroable from '@poppinss/macroable' +import type { HttpContext } from './http_context/main.ts' /** * Provides a fluent API for constructing HTTP redirect responses. @@ -48,6 +49,12 @@ import Macroable from '@poppinss/macroable' * ``` */ export class Redirect extends Macroable { + /** + * HTTP context reference, set by the response when creating + * the redirect instance during request handling. + */ + ctx?: HttpContext + /** * Array of allowed hosts for referrer-based redirects. * When empty, only the request's own host is allowed.
src/response.ts+1 −0 modified@@ -1039,6 +1039,7 @@ export class HttpResponse extends Macroable { statusCode: number = ResponseStatus.Found ): Redirect | void { const handler = new Redirect(this.request, this, this.#router, this.#qs, this.#config.redirect) + handler.ctx = this.ctx if (forwardQueryString) { handler.withQs()
tests/helpers/get_previous_url.spec.ts+62 −0 added@@ -0,0 +1,62 @@ +/* + * @adonisjs/http-server + * + * (c) AdonisJS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +import { test } from '@japa/runner' +import { getPreviousUrl } from '../../src/helpers.ts' + +test.group('getPreviousUrl', () => { + test('return fallback when no referer header is set', ({ assert }) => { + assert.equal(getPreviousUrl({}, [], '/fallback'), '/fallback') + }) + + test('return fallback when referer header is empty', ({ assert }) => { + assert.equal(getPreviousUrl({ referer: '' }, [], '/'), '/') + }) + + test('accept referer matching the host header', ({ assert }) => { + const headers = { referer: 'https://example.com/foo', host: 'example.com' } + assert.equal(getPreviousUrl(headers, [], '/'), 'https://example.com/foo') + }) + + test('accept referer matching an allowed host', ({ assert }) => { + const headers = { referer: 'https://admin.example.com/bar', host: 'example.com' } + assert.equal( + getPreviousUrl(headers, ['admin.example.com'], '/'), + 'https://admin.example.com/bar' + ) + }) + + test('return fallback when referer host does not match', ({ assert }) => { + const headers = { referer: 'https://evil.com/phish', host: 'example.com' } + assert.equal(getPreviousUrl(headers, [], '/'), '/') + }) + + test('return fallback for malformed referer', ({ assert }) => { + const headers = { referer: '//evil.com', host: 'example.com' } + assert.equal(getPreviousUrl(headers, [], '/'), '/') + }) + + test('accept relative referer URLs', ({ assert }) => { + const headers = { referer: '/foo/bar', host: 'example.com' } + assert.equal(getPreviousUrl(headers, [], '/'), '/foo/bar') + }) + + test('use the first value when referer is an array', ({ assert }) => { + const headers = { + referer: ['https://example.com/a', 'https://example.com/b'] as any, + host: 'example.com', + } + assert.equal(getPreviousUrl(headers, [], '/'), 'https://example.com/a') + }) + + test('use referrer header when referer is not set', ({ assert }) => { + const headers = { referrer: 'https://example.com/foo', host: 'example.com' } as any + assert.equal(getPreviousUrl(headers, [], '/'), 'https://example.com/foo') + }) +})
tests/helpers/is_valid_redirect_url.spec.ts+78 −0 added@@ -0,0 +1,78 @@ +/* + * @adonisjs/http-server + * + * (c) AdonisJS + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +import { test } from '@japa/runner' +import { isValidRedirectUrl } from '../../src/helpers.ts' + +test.group('isValidRedirectUrl', () => { + test('accept relative URLs starting with /', ({ assert }) => { + assert.isTrue(isValidRedirectUrl('/')) + assert.isTrue(isValidRedirectUrl('/foo')) + assert.isTrue(isValidRedirectUrl('/foo/bar')) + assert.isTrue(isValidRedirectUrl('/foo?bar=baz')) + assert.isTrue(isValidRedirectUrl('/foo#section')) + }) + + test('reject empty or non-string values', ({ assert }) => { + assert.isFalse(isValidRedirectUrl('')) + assert.isFalse(isValidRedirectUrl(' ')) + assert.isFalse(isValidRedirectUrl(null as any)) + assert.isFalse(isValidRedirectUrl(undefined as any)) + assert.isFalse(isValidRedirectUrl(123 as any)) + }) + + test('reject protocol-relative URLs', ({ assert }) => { + assert.isFalse(isValidRedirectUrl('//evil.com')) + assert.isFalse(isValidRedirectUrl('//evil.com/path')) + }) + + test('reject relative URLs that trick the URL parser', ({ assert }) => { + assert.isFalse(isValidRedirectUrl('/\\evil.com')) + }) + + test('accept absolute URLs when no host constraints are provided', ({ assert }) => { + assert.isTrue(isValidRedirectUrl('https://example.com')) + assert.isTrue(isValidRedirectUrl('https://example.com/path')) + assert.isTrue(isValidRedirectUrl('http://localhost:3000/foo')) + }) + + test('reject malformed absolute URLs', ({ assert }) => { + assert.isFalse(isValidRedirectUrl('not-a-url')) + assert.isFalse(isValidRedirectUrl('ftp://')) + }) + + test('accept absolute URLs matching currentHost', ({ assert }) => { + assert.isTrue(isValidRedirectUrl('https://example.com/foo', 'example.com')) + assert.isTrue(isValidRedirectUrl('http://localhost:3000/foo', 'localhost:3000')) + }) + + test('reject absolute URLs not matching currentHost', ({ assert }) => { + assert.isFalse(isValidRedirectUrl('https://evil.com/foo', 'example.com')) + assert.isFalse(isValidRedirectUrl('https://example.com:8080/foo', 'example.com')) + }) + + test('accept absolute URLs in allowedHosts', ({ assert }) => { + assert.isTrue(isValidRedirectUrl('https://app.example.com/foo', undefined, ['app.example.com'])) + assert.isTrue( + isValidRedirectUrl('https://admin.example.com/foo', 'app.example.com', [ + 'admin.example.com', + ]) + ) + }) + + test('reject absolute URLs not in allowedHosts or currentHost', ({ assert }) => { + assert.isFalse( + isValidRedirectUrl('https://evil.com/foo', 'example.com', ['app.example.com']) + ) + }) + + test('relative URLs ignore host constraints', ({ assert }) => { + assert.isTrue(isValidRedirectUrl('/foo', 'example.com', ['app.example.com'])) + }) +})
Vulnerability mechanics
Generated by null/stub on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
6- github.com/adonisjs/http-server/commit/2008fb6cf4f6f1c0ca5797d57def4d93e1c3de08nvdPatchWEB
- github.com/adonisjs/http-server/security/advisories/GHSA-6qvv-pj99-48qmnvdPatchVendor AdvisoryWEB
- github.com/advisories/GHSA-6qvv-pj99-48qmghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2026-40255ghsaADVISORY
- github.com/adonisjs/http-server/releases/tag/v7.8.1nvdRelease NotesWEB
- github.com/adonisjs/http-server/releases/tag/v8.2.0nvdRelease NotesWEB
News mentions
0No linked articles in our index yet.