fix: handle incomplete href attributes in workflow email links (#27094)
* handle incomplete href * addrss cubic comment
This commit is contained in:
@@ -125,6 +125,15 @@ const escapeHtml = (str: string): string =>
|
||||
.replace(/>/g, ">")
|
||||
.replace(/"/g, """);
|
||||
|
||||
/**
|
||||
* Escapes HTML attribute values to prevent attribute injection.
|
||||
*/
|
||||
const escapeHtmlAttribute = (str: string): string =>
|
||||
str
|
||||
.replace(/&/g, "&")
|
||||
.replace(/"/g, """)
|
||||
.replace(/'/g, "'");
|
||||
|
||||
/**
|
||||
* Replaces cloaked links in HTML content with visible URLs.
|
||||
* This ensures recipients can see the actual destination of links,
|
||||
@@ -134,6 +143,7 @@ const escapeHtml = (str: string): string =>
|
||||
* Into: <a href="https://example.com">https://example.com</a>
|
||||
*
|
||||
* Also handles nested HTML tags like: <a href="https://example.com"><b>Click here</b></a>
|
||||
* Handles incomplete hrefs: <a href="https://">https://example.com</a>
|
||||
*/
|
||||
export const replaceCloakedLinksInHtml = (html: string): string => {
|
||||
// Match anchor tags with href attribute
|
||||
@@ -142,18 +152,34 @@ export const replaceCloakedLinksInHtml = (html: string): string => {
|
||||
|
||||
return html.replace(anchorRegex, (match, attributes, href, innerContent) => {
|
||||
// Strip HTML tags from inner content to get plain text for comparison
|
||||
const linkText = innerContent.replace(/<[^>]*>/g, "");
|
||||
const linkText = innerContent.replace(/<[^>]*>/g, "").trim();
|
||||
|
||||
// Check if href is incomplete (just protocol like "https://" or "http://")
|
||||
const isIncompleteHref = /^https?:\/\/?$/.test(href);
|
||||
|
||||
// If href is incomplete and link text contains a valid URL, use the link text URL
|
||||
let actualUrl = href;
|
||||
let hrefWasUpdated = false;
|
||||
if (isIncompleteHref && linkText) {
|
||||
const urlPattern = /^https?:\/\/[^\s]+/i;
|
||||
const urlMatch = linkText.match(urlPattern);
|
||||
if (urlMatch) {
|
||||
actualUrl = urlMatch[0];
|
||||
attributes = attributes.replace(/href=["'][^"']+["']/, `href="${escapeHtmlAttribute(actualUrl)}"`);
|
||||
hrefWasUpdated = true;
|
||||
}
|
||||
}
|
||||
|
||||
// If the link text is already the URL (or very similar), keep it as is
|
||||
const normalizedHref = href.toLowerCase().replace(/\/$/, "");
|
||||
const normalizedHref = actualUrl.toLowerCase().replace(/\/$/, "");
|
||||
const normalizedText = linkText.toLowerCase().trim().replace(/\/$/, "");
|
||||
|
||||
if (normalizedText === normalizedHref || normalizedText === normalizedHref.replace(/^https?:\/\//, "")) {
|
||||
if (!hrefWasUpdated && (normalizedText === normalizedHref || normalizedText === normalizedHref.replace(/^https?:\/\//, ""))) {
|
||||
return match;
|
||||
}
|
||||
|
||||
// Replace the link text with the actual URL, escaping HTML to prevent XSS
|
||||
return `<a ${attributes}>${escapeHtml(href)}</a>`;
|
||||
return `<a ${attributes}>${escapeHtml(actualUrl)}</a>`;
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -124,6 +124,63 @@ describe("replaceCloakedLinksInHtml", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("incomplete hrefs", () => {
|
||||
test("should use URL from link text when href is incomplete (https://)", () => {
|
||||
const html =
|
||||
'<a href="https://" rel="noopener" class="editor-link"><span>https://bristolwills.com/prepare-for-will-appointment</span><br></a>';
|
||||
const result = replaceCloakedLinksInHtml(html);
|
||||
expect(result).toBe(
|
||||
'<a href="https://bristolwills.com/prepare-for-will-appointment" rel="noopener" class="editor-link">https://bristolwills.com/prepare-for-will-appointment</a>'
|
||||
);
|
||||
});
|
||||
|
||||
test("should use URL from link text when href is incomplete (http://)", () => {
|
||||
const html = '<a href="http://">http://example.com/page</a>';
|
||||
const result = replaceCloakedLinksInHtml(html);
|
||||
expect(result).toBe('<a href="http://example.com/page">http://example.com/page</a>');
|
||||
});
|
||||
|
||||
test("should handle incomplete href with nested HTML tags", () => {
|
||||
const html = '<a href="https://"><span>https://example.com</span></a>';
|
||||
const result = replaceCloakedLinksInHtml(html);
|
||||
expect(result).toBe('<a href="https://example.com">https://example.com</a>');
|
||||
});
|
||||
|
||||
test("should handle incomplete href with additional attributes", () => {
|
||||
const html =
|
||||
'<a href="https://" target="_blank" rel="noopener">https://example.com/path</a>';
|
||||
const result = replaceCloakedLinksInHtml(html);
|
||||
expect(result).toBe(
|
||||
'<a href="https://example.com/path" target="_blank" rel="noopener">https://example.com/path</a>'
|
||||
);
|
||||
});
|
||||
|
||||
test("should not modify if link text doesn't contain a valid URL", () => {
|
||||
const html = '<a href="https://">Click here</a>';
|
||||
const result = replaceCloakedLinksInHtml(html);
|
||||
expect(result).toBe('<a href="https://">https://</a>');
|
||||
});
|
||||
|
||||
test("should handle incomplete href when link text already matches", () => {
|
||||
const html = '<a href="https://">https://example.com</a>';
|
||||
const result = replaceCloakedLinksInHtml(html);
|
||||
expect(result).toBe('<a href="https://example.com">https://example.com</a>');
|
||||
});
|
||||
|
||||
test("should escape quotes in URL when fixing incomplete href to prevent attribute injection", () => {
|
||||
const html = '<a href="https://">https://example.com?q="test"</a>';
|
||||
const result = replaceCloakedLinksInHtml(html);
|
||||
expect(result).toBe('<a href="https://example.com?q="test"">https://example.com?q="test"</a>');
|
||||
});
|
||||
|
||||
test("should escape single quotes in URL when fixing incomplete href attribute", () => {
|
||||
const html = "<a href='https://'>https://example.com?q='test'</a>";
|
||||
const result = replaceCloakedLinksInHtml(html);
|
||||
// Single quotes in href attribute are escaped, but in link text they don't need escaping
|
||||
expect(result).toBe("<a href=\"https://example.com?q='test'\">https://example.com?q='test'</a>");
|
||||
});
|
||||
});
|
||||
|
||||
describe("edge cases", () => {
|
||||
test("should handle links with single quotes in href", () => {
|
||||
const html = "<a href='https://example.com'>Click here</a>";
|
||||
|
||||
Reference in New Issue
Block a user