Skip to content

fix(history/html5): avoid SecurityError on documents with userinfo URL - #2726

Open
spokodev wants to merge 2 commits into
vuejs:mainfrom
spokodev:fix/html5-replace-state-userinfo-security-error
Open

fix(history/html5): avoid SecurityError on documents with userinfo URL#2726
spokodev wants to merge 2 commits into
vuejs:mainfrom
spokodev:fix/html5-replace-state-userinfo-security-error

Conversation

@spokodev

@spokodev spokodev commented May 29, 2026

Copy link
Copy Markdown

When a Vue 3 app boots behind HTTP basic auth, the document URL carries userinfo (http://user:pass@host/`). The initial replaceState from createWebHistory() builds an absolute URL via location.protocol + '//' + location.host + base + to, and the browser sees that URL as cross-origin against the userinfo-bearing document and throws SecurityError. vue-router catches it and falls back to location.replace(url), which does a full reload, so the user-facing symptom is "the app reloads itself on every router push" and every subsequent client-side navigation also reloads.

Repro and full DevTools traces are in #2714. Posva's note on the issue suggested using createBaseLocation() only for file:// and an absolute (path-relative) URL for the others. The patch implements that suggestion in a slightly narrower form: it only drops the host prefix when the document URL has userinfo, so behaviour on the default code path is unchanged.

Change

In pushState inside packages/router/src/history/html5.ts, the URL constructed for non-hash bases is now path-relative when the document URL has userinfo. createBaseLocation() is still used for protocol-relative paths (//foo) so the existing 'prepends the host to support // urls' behaviour is preserved.

new URL(location.href).username is used instead of location.username because happy-dom (used by the test environment) does not implement location.username yet; the standard URL parser does, and the difference is invisible in real browsers.

Tests

Four cases under a new document URL with userinfo (HTTP basic auth) describe block in packages/router/tests/history/html5.spec.ts:

  • positive: with userinfo URL, push('/foo') passes /foo to pushState instead of http://localhost:3000/foo`. Fails on main because the current path always prepends the host.
  • edge: with userinfo URL, push('//foo') still resolves to http://localhost:3000//foo` so the existing // protocol-relative escape hatch is not broken.
  • regression guard: with a plain document URL, push('/foo') still produces http://localhost:3000/foo` (the existing 'prepends the host to support // urls' assertion remains green).
  • regression guard: file:// document URL, push('/foo') still produces file:///foo.

Verification

pnpm vitest run tests/history/html5.spec.ts -> 19 passed (15 existing + 4 new).
pnpm vitest run tests -> 682 passed, 3 skipped, 2 todo (no regressions).
pnpm lint clean on the changed files.

(pnpm run test:types requires a prior build and fails the same way on main without one, so I have not gated on it locally - it is the standard test-dts/experimental.test-d.ts "Cannot find module 'vue-router'" infra issue.)

Fixes #2714.

Summary by CodeRabbit

  • Bug Fixes
    • Resolved a browser SecurityError that could occur when navigating with URLs that include HTTP basic-auth credentials. History navigation now correctly uses safe, relative paths for those document URL formats.
  • Tests
    • Added regression coverage for document URLs containing userinfo, including checks for correct pushState/replaceState relative path behavior and unchanged handling for protocol-relative and file URL cases.

When a Vue 3 app boots behind HTTP basic auth, the document URL
carries userinfo (http://user:pass@host/). The existing
createWebHistory() initial replaceState builds an absolute URL via
location.protocol + '//' + location.host + base + to, and the
browser then sees that URL as cross-origin against the document URL
and throws SecurityError. vue-router catches the error and falls
back to location.replace(url), which performs a full reload, so the
user-facing symptom is "the app reloads itself on every router push"
and any subsequent client-side navigation also reloads. See vuejs#2714.

Stripping the host from the constructed URL when the document URL
has userinfo lets pushState resolve the path against the current
document origin, which avoids the cross-origin check entirely. The
existing // protocol-relative escape hatch still routes through
createBaseLocation() so '/foo' vs '//foo' keep their current
semantics.

The behavior change is scoped to documents whose URL has userinfo
(detected via new URL(location.href).username because happy-dom does
not implement location.username). All other code paths keep
prepending location.protocol + '//' + location.host as before, so
the existing 'prepends the host to support // urls' assertion and
the file:// hash-base tests are unaffected.

Tests cover:
- positive: with userinfo URL, push('/foo') passes '/foo' to
  pushState instead of 'http://localhost:3000/foo'
- edge: with userinfo URL, push('//foo') still resolves to
  'http://localhost:3000//foo' so // urls are not broken
- regression guard: with a plain document URL push('/foo') still
  yields 'http://localhost:3000/foo'
- regression guard: file:// document URL push('/foo') still yields
  'file:///foo'

Fixes vuejs#2714.
@netlify

netlify Bot commented May 29, 2026

Copy link
Copy Markdown

Deploy Preview for vue-router canceled.

Name Link
🔨 Latest commit 28d1fa1
🔍 Latest deploy log https://app.netlify.com/projects/vue-router/deploys/6a342d3d1ffae4000814f37d

@coderabbitai

coderabbitai Bot commented May 29, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 3ced342b-f275-4142-b3a2-7c9b1167c479

📥 Commits

Reviewing files that changed from the base of the PR and between 8eef1b3 and 28d1fa1.

📒 Files selected for processing (1)
  • packages/router/__tests__/history/html5.spec.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/router/tests/history/html5.spec.ts

📝 Walkthrough

Walkthrough

changeLocation in html5.ts is updated to compute an intermediate path (base + to) and omit the absolute createBaseLocation() prefix when the document URL contains HTTP basic-auth userinfo and the path does not begin with //. A new test suite in html5.spec.ts covers the resulting URL passed to pushState for userinfo, no-userinfo, protocol-relative, and file: URL cases.

Changes

SecurityError fix for userinfo URLs in changeLocation

Layer / File(s) Summary
changeLocation URL construction fix and tests
packages/router/src/history/html5.ts, packages/router/__tests__/history/html5.spec.ts
changeLocation introduces a path variable (base + to) and conditionally skips prepending createBaseLocation() when new URL(location.href).username is non-empty and path does not start with //, producing a relative URL for pushState. The new describe block adds tests asserting relative output for userinfo documents (http://user:pass@.../foo), preserved protocol-relative output for //foo, existing absolute output for no-userinfo HTTP documents, and file: URL absolute output.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~12 minutes

Poem

🐇 Hippity-hop, a secret in the URL,
user:pass@ hiding, causing quite a furl!
No more SecurityError crashing the page,
A relative path soothes Chromium's rage.
The router hops cleanly, no reload in sight —
Basic-auth staging works, and all feels right! 🥕

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the main fix: avoiding SecurityError on documents with userinfo URLs in the html5 history module.
Linked Issues check ✅ Passed The code changes directly address all requirements from issue #2714: detecting userinfo in document URLs and using path-relative URLs to prevent SecurityError during initial replaceState.
Out of Scope Changes check ✅ Passed All changes are directly scoped to fixing the SecurityError issue with userinfo URLs; test additions validate the fix without introducing unrelated functionality.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@spokodev
spokodev marked this pull request as ready for review June 18, 2026 16:37

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
packages/router/__tests__/history/html5.spec.ts (1)

109-123: ⚡ Quick win

Add a direct assertion for the initialization replaceState path.

The original failure happened during createWebHistory() initialization, but this test currently starts spying after that call. Adding a pre-init replaceState assertion would pin the regression at the root trigger.

Proposed test adjustment
     it('passes a relative URL to push/replaceState to avoid SecurityError', () => {
       getWindow().happyDOM.setURL('http://test:test@localhost:3000/')
+      const replaceSpy = vi.spyOn(window.history, 'replaceState')
       let history = createWebHistory()
+      expect(replaceSpy).toHaveBeenCalledWith(
+        expect.anything(),
+        expect.any(String),
+        '/'
+      )
+      replaceSpy.mockRestore()
       let spy = vi.spyOn(window.history, 'pushState')
       history.push('/foo')
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/router/__tests__/history/html5.spec.ts` around lines 109 - 123, The
test for the relative URL handling needs to verify that `replaceState` is called
with a relative URL during `createWebHistory()` initialization, not just after.
Move the spy setup for `window.history.replaceState` (and possibly
`window.history.pushState`) to occur before the `createWebHistory()` call in
this test function, then add an assertion to verify that the `replaceState` spy
was called with a relative URL during initialization. This will catch the
regression at its root trigger point rather than only checking the subsequent
`push()` call.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@packages/router/__tests__/history/html5.spec.ts`:
- Around line 109-123: The test for the relative URL handling needs to verify
that `replaceState` is called with a relative URL during `createWebHistory()`
initialization, not just after. Move the spy setup for
`window.history.replaceState` (and possibly `window.history.pushState`) to occur
before the `createWebHistory()` call in this test function, then add an
assertion to verify that the `replaceState` spy was called with a relative URL
during initialization. This will catch the regression at its root trigger point
rather than only checking the subsequent `push()` call.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: a8e3cc84-c956-4ebe-a2f6-97a9dd61199b

📥 Commits

Reviewing files that changed from the base of the PR and between 61f0e6b and 8eef1b3.

📒 Files selected for processing (2)
  • packages/router/__tests__/history/html5.spec.ts
  • packages/router/src/history/html5.ts

The original SecurityError in vuejs#2714 was thrown by createWebHistory() during
initialization, before any navigation. The existing tests start spying after
that call, so they cover push but not the init path. Spy on replaceState
before createWebHistory() and assert it receives a relative URL, pinning the
regression at its real trigger.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

createWebHistory() initial replaceState throws SecurityError when document URL contains userinfo (HTTP basic-auth in URL)

1 participant