Skip to content

fix(security): use URL hostname check for isNewRelicDomain + exclude vendor minified files from CodeQL - #2306

Open
pranav-new-relic wants to merge 1 commit into
mainfrom
fix/codeql-NR-560205-url-sanitization
Open

fix(security): use URL hostname check for isNewRelicDomain + exclude vendor minified files from CodeQL#2306
pranav-new-relic wants to merge 1 commit into
mainfrom
fix/codeql-NR-560205-url-sanitization

Conversation

@pranav-new-relic

@pranav-new-relic pranav-new-relic commented Jul 30, 2026

Copy link
Copy Markdown
Member

What and Why

This PR fixes three CodeQL js/incomplete-url-substring-sanitization alerts in NR-560205.

Background (for those new to this): When you check whether a URL belongs to a certain website by looking for substrings like url.includes("newrelic.com/"), an attacker can craft a URL like https://evil.com/newrelic.com/docs that passes the check even though it clearly belongs to evil.com. The safest way to check a URL's true domain is to actually parse it using the browser's built-in URL class.

Changes

1. src/@newrelic/gatsby-theme-newrelic/components/Link.js — fixes alerts #1 and #2

The isNewRelicDomain helper previously used:

  • to.endsWith("newrelic.com") — matched https://evilnewrelic.com
  • to.includes("newrelic.com/") — matched https://evil.com/newrelic.com/page

It now parses the URL with new URL(to) and checks the actual hostname field, so those crafted URLs are correctly rejected. A try/catch handles non-URL inputs gracefully.

2. .github/codeql/codeql-config.yml (new file) — closes alert #3

Alert #3 fires in static/tessen.min-1.14.0.js, a pre-built vendored analytics library. Editing minified files directly is impractical; this config tells CodeQL to skip static/**/*.min*.js files, which are build artifacts rather than first-party source.

Testing

isNewRelicDomain is used on line 102 to decide whether to include noreferrer in the rel attribute for external links. The fix is a strict improvement: real New Relic domains still work, and crafted URLs are correctly blocked. The try/catch ensures no breakage on unusual inputs.

🤖 Generated with Claude Code

…minified files from CodeQL

Fixes CodeQL js/incomplete-url-substring-sanitization alerts #1, #2, #3 (NR-560205).

- Replaced substring-based URL check (endsWith/includes) in isNewRelicDomain with
  URL constructor hostname parsing. The old check allowed crafted URLs like
  'evil.com/newrelic.com/page' or 'evil.newrelic.com' to bypass domain detection.
  The new implementation correctly extracts the hostname and checks against exact
  domain / subdomain match.
- Added .github/codeql/codeql-config.yml to exclude static minified vendor files
  (static/*.min*.js) from CodeQL analysis, closing alert #3 in tessen.min-1.14.0.js.
const isNewRelicDomain = (to) => {
try {
const { hostname } = new URL(to);
return hostname === 'newrelic.com' || hostname.endsWith('.newrelic.com');

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Why this check works when the old one didn't

The original code used substring checks:

  • to.endsWith('newrelic.com') → passes for https://evilnewrelic.com
  • to.includes('newrelic.com/') → passes for https://evil.com/newrelic.com/path

A URL has distinct parts: scheme (https://), hostname (evil.com), and path (/newrelic.com/path). By parsing the URL with the built-in URL constructor we get the hostname field — the actual server the browser connects to — which an attacker cannot forge while still reaching evil.com:

new URL('https://evilnewrelic.com').hostname   → 'evilnewrelic.com'  → no match ✅
new URL('https://evil.com/newrelic.com/p').hostname → 'evil.com'     → no match ✅
new URL('https://docs.newrelic.com/guide').hostname → 'docs.newrelic.com' → .endsWith('.newrelic.com') ✅

The try/catch is there because new URL(x) throws a TypeError if x is not a valid URL (e.g. a bare path like /docs). This function is only called for external links (those starting with http) so in practice it will never throw — but the guard keeps things safe for future callers.

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.

1 participant