Skip to content

fix: reject non-http URL schemes on the anonymous check instead of mangling them - #83

Merged
nish3451 merged 4 commits into
mainfrom
seo-fix-kit/lane1-check-url-scheme-validation
Aug 10, 2026
Merged

nish3451 merged 4 commits into
mainfrom
seo-fix-kit/lane1-check-url-scheme-validation

Conversation

@nish3451

@nish3451 nish3451 commented Aug 10, 2026 •

Copy link
Copy Markdown
Member

What

The anonymous one-page check at /check accepted any URL prefix and mangled non-http schemes into garbage instead of rejecting them:

  • ftp://example.com -> normalized to https://ftp//example.com -> the public check 422s with a cryptic net::ERR_CONNECTION_RESET error
  • mailto:hello@example.com -> normalized to https://mailto:hello@example.com/ and silently audits example.com with a mangled displayed URL
  • https://user:pass@example.com/ -> accepted with embedded credentials

Verified live against the deployed Worker (pre-fix):

  • POST /api/public-check {"url":"ftp://example.com"} -> {"error": "net::ERR_CONNECTION_RESET at https://ftp//example.com"} (422)
  • POST /api/public-check {"url":"mailto:test@example.com"} -> {"ok": true, "checkedUrl": "https://mailto:test@example.com/", "finalUrl": "https://example.com/", ...} (silently audits the wrong host)

Fix

  • shared/audit-engine.js normalizeUrl (used by public check, audits, developer API, local server, competitor/backlink/keyword/local inputs) now throws on explicit non-http(s) schemes and on URLs carrying embedded credentials; every caller already maps that throw to Enter a valid public website URL. (400)
  • /check form input changed type="url" -> type="text" (with inputmode="url"): Chrome silently blocked scheme-less entries like example.com client-side with Please enter a URL. even though the server normalizes them fine

Pins

  • unit tests for normalizeUrl (scheme-less, protocol-relative, http(s) intact, ftp/javascript/mailto/credentials rejected)
  • route tests for validatePublicCheckUrl + the form input attributes
  • live-promise spot-check gains a scheme-rejection probe - currently red against the deployed Worker, green after this deploys

Verification

  • npm run check fully green (all suites + build)
  • focused suites green: shared/audit-engine.test.mjs, worker/routes/public-check.test.mjs, scripts/live-promise-spot-check.test.mjs (42/42)

Summary by CodeRabbit

  • Bug Fixes

    • Improved public URL validation to reject unsupported protocols and URLs containing usernames or passwords.
    • Protocol-relative and scheme-less website addresses are now accepted and normalized correctly.
    • Updated validation messaging to clearly reference valid public website URLs.
    • URL entry now supports scheme-less addresses without browser-side blocking.
  • Tests

    • Expanded coverage for URL formats, normalization, and invalid public-check requests.

…ngling them

The public one-page check normalized any non-http scheme into garbage
instead of rejecting it: ftp://example.com became https://ftp//example.com
(422 with a cryptic net::ERR_CONNECTION_RESET error) and mailto:hello@example.com
silently audited example.com with a mangled displayed URL. The shared
normalizeUrl also accepted embedded credentials. Every surface that uses
normalizeUrl (public check, audits, developer API, local server) now
rejects unsupported schemes and credential-bearing URLs with the standard
invalid-URL error.

The /check form input type=url also blocked scheme-less entries
("example.com") client-side with no message even though the server
normalizes them; the input now accepts plain text with inputmode=url so
the server's normalization runs.

- normalizeUrl throws on explicit non-http(s) schemes and embedded credentials
- /check input is type=text inputmode=url, pinned in route tests
- unit pins for normalizeUrl and route pins for validatePublicCheckUrl
- live-promise spot-check gains a scheme-rejection probe (red against the
  currently deployed Worker, green after deploy)

@greptile-apps greptile-apps Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

nish3451 has reached the 50-credit limit for trial accounts. To continue receiving code reviews, upgrade your plan.

@coderabbitai

coderabbitai Bot commented Aug 10, 2026 •

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@nish3451, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 22 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: b3da2961-e74a-4f14-a9de-caa94002d2b1

📥 Commits

Reviewing files that changed from the base of the PR and between c24a4b3 and 03a0621.

📒 Files selected for processing (3)
  • scripts/live-promise-spot-check.mjs
  • worker/routes/public-check.js
  • worker/routes/public-check.test.mjs
📝 Walkthrough

Walkthrough

The PR hardens URL normalization, changes the public-check field to text input, expands validation tests for unsupported schemes and credentials, and adds a live FTP rejection spot-check.

Changes

Public URL validation

Layer / File(s) Summary
URL normalization rules
shared/audit-engine.js, shared/audit-engine.test.mjs
normalizeUrl rejects unsupported schemes and embedded credentials. Tests cover valid normalization and rejection cases.
Public-check input and validation
worker/routes/public-check.js, worker/routes/public-check.test.mjs
The form uses text input with URL input mode. Tests cover FTP, mailto:, credentials, scheme-relative URLs, and rendered input attributes.
Live validation checks
scripts/live-promise-spot-check.mjs, scripts/live-promise-spot-check.test.mjs
The live check verifies FTP rejection and updates expected error text, spot-check entries, result counts, and stale-worker fixtures.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Possibly related PRs

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the main change: rejecting non-HTTP URL schemes instead of altering them.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches 💡 1
🛠️ Fix failing CI checks 💡
  • Create stacked PR
  • Commit on current branch
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch seo-fix-kit/lane1-check-url-scheme-validation

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

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: c24a4b3208

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread shared/audit-engine.js
Comment on lines +2724 to +2725
const explicitScheme = /^([a-z][a-z0-9+.-]*):\/\//i.exec(trimmed);
if (explicitScheme && !/^https?$/i.test(explicitScheme[1])) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Reject schemes that omit the double slash

The scheme check only recognizes forms containing ://, but URI schemes do not universally use that delimiter. For example, tel:1234 bypasses this check, is normalized to https://tel:1234/, and passes publicAuditUrlStatus, so the anonymous check still attempts a browser render and returns a runtime error instead of the intended 400 response. Detect the scheme from the leading name: portion before adding HTTPS.

Useful? React with 👍 / 👎.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 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.

Inline comments:
In `@shared/audit-engine.js`:
- Around line 2724-2728: Update the URL normalization logic around
explicitScheme and withProtocol to detect any non-HTTP scheme using the full
scheme: prefix, including single-slash and no-slash forms, before prepending
https://. Preserve host:port inputs such as example.com:8080/page, but ensure
the exception does not allow scheme-like values such as javascript:123.

In `@shared/audit-engine.test.mjs`:
- Around line 772-778: Update normalizeUrl’s scheme detection to reject explicit
non-HTTP schemes, including mailto and single-slash forms, before prepending
https://; emit Unsupported URL scheme "<scheme>" for these inputs. Preserve
example.com:8080/page as a host with a port, and extend the normalizeUrl tests
with mailto:, mailto:/hello, and javascript:/alert(1) cases.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: f036c2cc-de0b-47eb-a074-24e0858ffd82

📥 Commits

Reviewing files that changed from the base of the PR and between 120c699 and c24a4b3.

📒 Files selected for processing (6)
  • scripts/live-promise-spot-check.mjs
  • scripts/live-promise-spot-check.test.mjs
  • shared/audit-engine.js
  • shared/audit-engine.test.mjs
  • worker/routes/public-check.js
  • worker/routes/public-check.test.mjs

Comment thread shared/audit-engine.js
Comment on lines +2724 to 2728
const explicitScheme = /^([a-z][a-z0-9+.-]*):\/\//i.exec(trimmed);
if (explicitScheme && !/^https?$/i.test(explicitScheme[1])) {
throw new Error(`Unsupported URL scheme "${explicitScheme[1]}".`);
}
const withProtocol = /^https?:\/\//i.test(trimmed) ? trimmed : `https://${trimmed}`;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -eu

printf '%s\n' '--- target context ---'
sed -n '2670,2775p' shared/audit-engine.js

printf '%s\n' '--- relevant symbols and call sites ---'
rg -n -C 3 'explicitScheme|withProtocol|new URL|DNS|dns|audit' shared/audit-engine.js

printf '%s\n' '--- related tests and documentation ---'
rg -n -i -C 3 'example\.com:8080|unsupported URL scheme|javascript:|mailto:|data:|URL validation|audit-engine' . \
  -g '!node_modules' -g '!dist' -g '!build'

Repository: nish3451/seo-fix-kit

Length of output: 50376


🏁 Script executed:

#!/bin/bash
set -eu

printf '%s\n' '--- audit-engine URL tests ---'
rg -n -C 5 'normalizeUrl|parseAuditCompetitorUrls|publicAuditUrlStatus|competitorUrls' shared/audit-engine.test.mjs

printf '%s\n' '--- URL guard implementation ---'
sed -n '2770,2830p' shared/audit-engine.js

printf '%s\n' '--- standalone behavior probe ---'
node - <<'JS'
const inputs = [
  "javascript:/alert(1)",
  "mailto:/hello",
  "data:/text/plain",
  "example.com:8080/page",
  "javascript:123",
  "javascript:123/page",
  "https://example.com/page",
  "ftp://example.com/page"
];

const currentScheme = /^([a-z][a-z0-9+.-]*):\\/\\//i;
const currentHttp = /^https?:\\/\\//i;
const proposedHostPort = /^[a-z0-9.-]+:\\d+(?:[/?#]|$)/i;
const proposedScheme = /^([a-z][a-z0-9+.-]*):/i;

for (const input of inputs) {
  const trimmed = input.trim();
  const currentMatch = currentScheme.exec(trimmed);
  const currentRejected = Boolean(currentMatch && !/^https?$/i.test(currentMatch[1]));
  const currentValue = currentRejected ? null : (currentHttp.test(trimmed) ? trimmed : `https://${trimmed}`);
  let currentParsed = null;
  try {
    currentParsed = currentValue && new URL(currentValue);
  } catch {}
  const proposedMatch = proposedScheme.exec(trimmed);
  const proposedRejected = Boolean(
    proposedMatch &&
    !proposedHostPort.test(trimmed) &&
    !/^https?$/i.test(proposedMatch[1])
  );
  const proposedValue = proposedRejected ? null : (currentHttp.test(trimmed) ? trimmed : `https://${trimmed}`);
  let proposedParsed = null;
  try {
    proposedParsed = proposedValue && new URL(proposedValue);
  } catch {}
  console.log(JSON.stringify({
    input,
    current: currentParsed && { href: currentParsed.href, protocol: currentParsed.protocol, hostname: currentParsed.hostname, port: currentParsed.port },
    proposed: proposedRejected ? { rejected: true } : proposedParsed && { href: proposedParsed.href, protocol: proposedParsed.protocol, hostname: proposedParsed.hostname, port: proposedParsed.port }
  }));
}
JS

Repository: nish3451/seo-fix-kit

Length of output: 4512


🏁 Script executed:

#!/bin/bash
set -eu

node - <<'JS'
const inputs = [
  "javascript:/alert(1)",
  "mailto:/hello",
  "data:/text/plain",
  "example.com:8080/page",
  "javascript:123",
  "javascript:123/page",
  "https://example.com/page",
  "ftp://example.com/page"
];

const currentScheme = /^([a-z][a-z0-9+.-]*):\/\//i;
const currentHttp = /^https?:\/\//i;
const proposedHostPort = /^[a-z0-9.-]+:\d+(?:[/?#]|$)/i;
const proposedScheme = /^([a-z][a-z0-9+.-]*):/i;

for (const input of inputs) {
  const trimmed = input.trim();

  const currentMatch = currentScheme.exec(trimmed);
  const currentRejected = Boolean(currentMatch && !/^https?$/i.test(currentMatch[1]));
  const currentValue = currentRejected
    ? null
    : (currentHttp.test(trimmed) ? trimmed : `https://${trimmed}`);
  let currentParsed = null;
  try {
    currentParsed = currentValue && new URL(currentValue);
  } catch {}

  const proposedMatch = proposedScheme.exec(trimmed);
  const proposedRejected = Boolean(
    proposedMatch &&
    !proposedHostPort.test(trimmed) &&
    !/^https?$/i.test(proposedMatch[1])
  );
  const proposedValue = proposedRejected
    ? null
    : (currentHttp.test(trimmed) ? trimmed : `https://${trimmed}`);
  let proposedParsed = null;
  try {
    proposedParsed = proposedValue && new URL(proposedValue);
  } catch {}

  console.log(JSON.stringify({
    input,
    current: currentRejected
      ? { rejected: true, reason: "scheme check" }
      : currentParsed && {
          href: currentParsed.href,
          protocol: currentParsed.protocol,
          hostname: currentParsed.hostname,
          port: currentParsed.port
        },
    proposed: proposedRejected
      ? { rejected: true, reason: "scheme check" }
      : proposedParsed && {
          href: proposedParsed.href,
          protocol: proposedParsed.protocol,
          hostname: proposedParsed.hostname,
          port: proposedParsed.port
        }
  }));
}
JS

Repository: nish3451/seo-fix-kit

Length of output: 1805


Reject non-HTTP schemes before adding https://.

The scheme:// check lets javascript:/alert(1), mailto:/hello, and data:/text/plain become HTTPS URLs for hosts named javascript, mailto, and data. Apply the check to the full scheme: prefix. Keep the example.com:8080/page host-port form, but do not let the exception admit scheme-like inputs such as javascript:123.

🧰 Tools
🪛 OpenGrep (1.26.0)

[ERROR] 2724-2724: Dynamic command passed to child_process.exec/execSync. Use child_process.execFile or spawn with an argument array instead.

(coderabbit.command-injection.exec-js)

🤖 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 `@shared/audit-engine.js` around lines 2724 - 2728, Update the URL
normalization logic around explicitScheme and withProtocol to detect any
non-HTTP scheme using the full scheme: prefix, including single-slash and
no-slash forms, before prepending https://. Preserve host:port inputs such as
example.com:8080/page, but ensure the exception does not allow scheme-like
values such as javascript:123.

Comment on lines +772 to +778
test("normalizeUrl rejects non-http schemes and embedded credentials instead of mangling them", () => {
assert.throws(() => normalizeUrl("ftp://example.com"), /Unsupported URL scheme "ftp"/);
assert.throws(() => normalizeUrl("javascript://example.com/x"), /Unsupported URL scheme "javascript"/);
assert.throws(() => normalizeUrl("mailto:hello@example.com"), /embedded credentials/i);
assert.throws(() => normalizeUrl("https://user:pass@example.com/"), /embedded credentials/i);
assert.throws(() => normalizeUrl("https://user@example.com/"), /embedded credentials/i);
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -e

printf '%s\n' '--- candidate files ---'
fd -i 'audit-engine|normalize' . | head -80

printf '%s\n' '--- normalizeUrl definitions and uses ---'
rg -n -C 8 'normalizeUrl|Unsupported URL scheme|embedded credentials' . --glob '!node_modules' --glob '!dist' --glob '!build'

Repository: nish3451/seo-fix-kit

Length of output: 29231


🏁 Script executed:

#!/bin/bash
set -e

printf '%s\n' '--- files ---'
git ls-files | rg '(^|/)(shared/)?audit-engine|normalize|package.json'

printf '%s\n' '--- relevant source ---'
rg -n -C 12 'function normalizeUrl|const normalizeUrl|normalizeUrl\s*=|Unsupported URL scheme|embedded credentials' . \
  --glob '!node_modules' --glob '!dist' --glob '!build'

Repository: nish3451/seo-fix-kit

Length of output: 9796


🏁 Script executed:

#!/bin/bash
set -e
rg -n -C 12 'normalizeUrl|Unsupported URL scheme|embedded credentials' . \
  --glob '!node_modules' --glob '!dist' --glob '!build'

Repository: nish3451/seo-fix-kit

Length of output: 40170


🏁 Script executed:

#!/bin/bash
set -e

node <<'JS'
function currentNormalizeUrl(input) {
  const trimmed = String(input || "").trim();
  const explicitScheme = /^([a-z][a-z0-9+.-]*):\/\//i.exec(trimmed);
  if (explicitScheme && !/^https?$/i.test(explicitScheme[1])) {
    throw new Error(`Unsupported URL scheme "${explicitScheme[1]}".`);
  }
  const withProtocol = /^https?:\/\//i.test(trimmed) ? trimmed : `https://${trimmed}`;
  const url = new URL(withProtocol);
  if (url.username || url.password) {
    throw new Error("URLs with embedded credentials are not supported.");
  }
  url.hash = "";
  return url.href;
}

for (const value of [
  "ftp://example.com",
  "javascript://example.com/x",
  "mailto:hello@example.com",
  "mailto:/hello",
  "javascript:/alert(1)",
  "https://user:pass@example.com/",
  "https://user@example.com/"
]) {
  try {
    console.log(JSON.stringify({ value, result: currentNormalizeUrl(value) }));
  } catch (error) {
    console.log(JSON.stringify({ value, error: error.message }));
  }
}
JS

Repository: nish3451/seo-fix-kit

Length of output: 746


Reject explicit non-HTTP schemes before adding https://. Extend scheme detection to cover mailto: and single-slash forms, while preserving example.com:8080/page as a host with a port. Expect Unsupported URL scheme "mailto" and add cases for mailto:/hello and javascript:/alert(1).

🤖 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 `@shared/audit-engine.test.mjs` around lines 772 - 778, Update normalizeUrl’s
scheme detection to reject explicit non-HTTP schemes, including mailto and
single-slash forms, before prepending https://; emit Unsupported URL scheme
"<scheme>" for these inputs. Preserve example.com:8080/page as a host with a
port, and extend the normalizeUrl tests with mailto:, mailto:/hello, and
javascript:/alert(1) cases.

@greptile-apps greptile-apps Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

nish3451 has reached the 50-credit limit for trial accounts. To continue receiving code reviews, upgrade your plan.

@greptile-apps greptile-apps Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

nish3451 has reached the 50-credit limit for trial accounts. To continue receiving code reviews, upgrade your plan.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: a928f19ff7

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread shared/audit-engine.js

export function normalizeUrl(input) {
const trimmed = String(input || "").trim();
const explicitScheme = /^([a-z][a-z0-9+.-]*):\/\//i.exec(trimmed);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Reject colon-only URI schemes before prepending HTTPS

Fresh evidence in this revision is that the new scheme matcher still requires ://: normalizeUrl("tel:1234") currently returns https://tel:1234/, which publicAuditUrlStatus accepts. Consequently, /api/public-check proceeds through quota accounting and attempts a browser audit instead of returning the intended 400 response; detect the leading scheme: form regardless of whether it includes slashes.

Useful? React with 👍 / 👎.

@greptile-apps greptile-apps Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

nish3451 has reached the 50-credit limit for trial accounts. To continue receiving code reviews, upgrade your plan.

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