Source code

Revision control

Copy as Markdown

Other Tools

Test Info: Warnings

/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
/**
* Unit tests for SecurityUtils.sys.mjs
*
* Tests URL normalization, eTLD validation, and ledger management:
* - normalizeUrl() - URL validation and normalization
* - areSameSite() - eTLD+1 validation
* - TabLedger - per-tab URL storage with TTL
* - SessionLedger - multi-tab ledger management
*
* Focus: Critical paths and edge cases that affect security
*/
const { normalizeUrl, TabLedger, SessionLedger } = ChromeUtils.importESModule(
"chrome://global/content/ml/security/SecurityUtils.sys.mjs"
);
/**
* Test: valid HTTP URLs normalize successfully.
*
* Reason:
* HTTP URLs are valid input for the security layer. The normalizer
* must accept them and return a normalized form for consistent
* ledger comparison.
*/
add_task(async function test_normalizeUrl_valid_http() {
const result = normalizeUrl("http://example.com/page");
Assert.ok(result.success, "Should succeed for valid HTTP URL");
Assert.ok(result.url, "Should return normalized URL");
Assert.ok(result.url.startsWith("http://"), "Should preserve http scheme");
});
/**
* Test: valid HTTPS URLs normalize successfully.
*
* Reason:
* HTTPS URLs are the primary input for the security layer. The normalizer
* must accept them and preserve the scheme in the output.
*/
add_task(async function test_normalizeUrl_valid_https() {
const result = normalizeUrl("https://example.com/page");
Assert.ok(result.success, "Should succeed for valid HTTPS URL");
Assert.ok(result.url, "Should return normalized URL");
Assert.ok(result.url.startsWith("https://"), "Should preserve https scheme");
});
/**
* Test: URLs with query parameters normalize successfully.
*
* Reason:
* Query parameters are part of resource identity. The normalizer must
* preserve them so that URLs like `page?id=1` and `page?id=2` are
* treated as distinct resources.
*/
add_task(async function test_normalizeUrl_with_query_params() {
const result = normalizeUrl("https://example.com/page?foo=bar&baz=qux");
Assert.ok(result.success, "Should succeed for URL with query params");
Assert.ok(result.url.includes("?"), "Should preserve query parameters");
});
/**
* Test: empty string fails normalization.
*
* Reason:
* Empty strings are invalid URLs. The normalizer must reject them
* with an error rather than returning an empty or malformed result.
*/
add_task(async function test_normalizeUrl_empty_string() {
const result = normalizeUrl("");
Assert.ok(!result.success, "Should fail for empty string");
Assert.ok(result.error, "Should return error");
});
/**
* Test: whitespace-only string fails normalization.
*
* Reason:
* Whitespace-only strings are invalid URLs. The normalizer must
* reject them rather than treating whitespace as a valid resource.
*/
add_task(async function test_normalizeUrl_whitespace() {
const result = normalizeUrl(" ");
Assert.ok(!result.success, "Should fail for whitespace-only string");
Assert.ok(result.error, "Should return error");
});
/**
* Test: invalid URL format fails normalization.
*
* Reason:
* Malformed URLs cannot be validated against the ledger. The normalizer
* must reject them so the security layer can deny the request (fail-closed).
*/
add_task(async function test_normalizeUrl_invalid_format() {
const result = normalizeUrl("not-a-valid-url");
Assert.ok(!result.success, "Should fail for invalid URL format");
Assert.ok(result.error, "Should return error");
});
/**
* Test: non-http/https schemes fail normalization.
*
* Reason:
* Only http/https URLs are valid for web content fetching. Schemes like
* ftp://, file://, and javascript: must be rejected to prevent attacks
* using unexpected protocol handlers.
*/
add_task(async function test_normalizeUrl_non_http_scheme() {
const schemes = ["ftp://example.com", "file:///path", "javascript:alert(1)"];
for (const url of schemes) {
const result = normalizeUrl(url);
Assert.ok(!result.success, `Should fail for scheme: ${url}`);
Assert.ok(result.error, "Should return error");
}
});
/**
* Test: null/undefined fail normalization gracefully.
*
* Reason:
* Defensive programming: the normalizer must handle null/undefined
* without throwing, returning a failure result instead.
*/
add_task(async function test_normalizeUrl_null_undefined() {
const resultNull = normalizeUrl(null);
const resultUndefined = normalizeUrl(undefined);
Assert.ok(!resultNull.success, "Should fail for null");
Assert.ok(!resultUndefined.success, "Should fail for undefined");
});
/**
* Test: fragments are removed during normalization.
*
* Reason:
* Fragments (#section) identify positions within a page, not different
* resources. Stripping them ensures `page` and `page#section` are treated
* as the same resource for security purposes.
*/
add_task(async function test_normalizeUrl_strips_fragments() {
const result = normalizeUrl("https://example.com/page#section");
Assert.ok(result.success, "Should succeed");
Assert.ok(!result.url.includes("#"), "Should strip fragment");
});
/**
* Test: tracking parameters are removed during normalization.
*
* Reason:
* Tracking parameters (utm_source, etc.) don't change the resource.
* Stripping them prevents false denials when the same page is accessed
* with different tracking parameters.
*/
add_task(async function test_normalizeUrl_strips_tracking() {
const result = normalizeUrl(
);
Assert.ok(result.success, "Should succeed");
Assert.ok(!result.url.includes("utm_"), "Should strip utm parameters");
Assert.ok(
result.url.includes("foo=bar"),
"Should preserve non-tracking params"
);
});
/**
* Test: relative URLs work with baseUrl.
*
* Reason:
* Page content may contain relative URLs. The normalizer must resolve
* them against a base URL to produce absolute URLs for ledger comparison.
*/
add_task(async function test_normalizeUrl_relative_with_base() {
const result = normalizeUrl("/page", "https://example.com");
Assert.ok(result.success, "Should succeed with baseUrl");
Assert.ok(
result.url.includes("example.com/page"),
"Should resolve relative URL"
);
});
/**
* Test: TabLedger can be created.
*
* Reason:
* TabLedger is the per-tab URL storage. It must initialize correctly
* with a tab ID and start empty.
*/
add_task(async function test_TabLedger_creation() {
const ledger = new TabLedger("tab-123");
Assert.ok(ledger, "Should create ledger");
Assert.equal(ledger.tabId, "tab-123", "Should store tab ID");
Assert.equal(ledger.size(), 0, "Should start empty");
});
/**
* Test: seed() adds multiple URLs to ledger.
*
* Reason:
* When a page loads, multiple URLs (page URL, linked resources) are
* seeded at once. seed() must add all valid URLs to the ledger.
*/
add_task(async function test_TabLedger_seed() {
const ledger = new TabLedger("tab-123");
ledger.seed(urls);
Assert.equal(
ledger.lookup("https://example.com"),
"Should return normalized URL"
);
Assert.equal(
ledger.lookup("https://example.com/page"),
"Should return normalized URL"
);
Assert.equal(ledger.size(), 2, "Should have correct size");
});
/**
* Test: add() adds individual URLs.
*
* Reason:
* Single URLs may be added incrementally (e.g., dynamic content).
* add() must work for individual URL additions.
*/
add_task(async function test_TabLedger_add() {
const ledger = new TabLedger("tab-123");
ledger.add("https://example.com");
Assert.equal(
ledger.lookup("https://example.com"),
"Should return normalized URL"
);
Assert.equal(ledger.size(), 1, "Should have size 1");
});
/**
* Test: has() returns false for URLs not in ledger.
*
* Reason:
* The core security check: has() must return false for unseen URLs
* so the policy can deny access to untrusted resources.
*/
add_task(async function test_TabLedger_has_missing() {
const ledger = new TabLedger("tab-123");
ledger.add("https://example.com");
Assert.equal(
ledger.lookup("https://evil.com"),
null,
"Should return null for missing URL"
);
});
/**
* Test: clear() empties the ledger.
*
* Reason:
* When a tab navigates to a new page, the old URLs are no longer
* valid. clear() must remove all URLs from the ledger.
*/
add_task(async function test_TabLedger_clear() {
const ledger = new TabLedger("tab-123");
ledger.clear();
Assert.equal(ledger.size(), 0, "Should be empty after clear");
Assert.equal(
ledger.lookup("https://example.com"),
null,
"Should return null after clear"
);
});
/**
* Test: ledger enforces size limit.
*
* Reason:
* Unbounded ledger growth could cause memory issues. The size limit
* prevents malicious pages from bloating the ledger with many URLs.
*/
add_task(async function test_TabLedger_size_limit() {
const maxUrls = 1000;
const ledger = new TabLedger("tab-123");
// Try to add more than max
for (let i = 0; i < maxUrls + 2; i++) {
ledger.add(`https://example.com/page${i}`);
}
Assert.lessOrEqual(ledger.size(), maxUrls, "Should not exceed max size");
});
/**
* Test: invalid URLs are rejected gracefully.
*
* Reason:
* Malformed URLs (empty strings, null, non-URLs) should be silently
* ignored rather than added to the ledger or causing exceptions.
*/
add_task(async function test_TabLedger_invalid_urls() {
const ledger = new TabLedger("tab-123");
ledger.add("not-a-url");
ledger.add("");
ledger.add(null);
Assert.equal(ledger.size(), 0, "Should not add invalid URLs");
});
/**
* Test: SessionLedger can be created.
*
* Reason:
* SessionLedger manages per-tab ledgers for a session. It must
* initialize with a session ID and start with no tabs.
*/
add_task(async function test_SessionLedger_creation() {
const session = new SessionLedger("session-123");
Assert.ok(session, "Should create session ledger");
Assert.equal(session.sessionId, "session-123", "Should store session ID");
Assert.equal(session.tabCount(), 0, "Should start with no tabs");
});
/**
* Test: forTab() creates and retrieves tab ledgers.
*
* Reason:
* forTab() is the primary interface for accessing tab ledgers. It must
* create a new ledger on first access and return the same instance
* on subsequent calls for the same tab.
*/
add_task(async function test_SessionLedger_forTab() {
const session = new SessionLedger("session-123");
const ledger1 = session.forTab("tab-1");
const ledger2 = session.forTab("tab-1"); // Same tab
Assert.ok(ledger1, "Should create ledger for tab-1");
Assert.equal(ledger1, ledger2, "Should return same ledger for same tab");
Assert.equal(session.tabCount(), 1, "Should have 1 tab");
});
/**
* Test: different tabs get different ledgers.
*
* Reason:
* Tab isolation: each tab must have its own ledger. URLs from one tab
* should not be automatically trusted in another tab.
*/
add_task(async function test_SessionLedger_multiple_tabs() {
const session = new SessionLedger("session-123");
const ledger1 = session.forTab("tab-1");
const ledger2 = session.forTab("tab-2");
Assert.notEqual(
ledger1,
ledger2,
"Different tabs should have different ledgers"
);
Assert.equal(session.tabCount(), 2, "Should have 2 tabs");
});
/**
* Test: merge() combines URLs from multiple tabs.
*
* Reason:
* The @mentions feature requires merging ledgers from multiple tabs.
* merge() must return a combined set of URLs from all specified tabs.
*/
add_task(async function test_SessionLedger_merge() {
const session = new SessionLedger("session-123");
const ledger1 = session.forTab("tab-1");
const ledger2 = session.forTab("tab-2");
ledger1.add("https://example.com/page1");
ledger2.add("https://example.com/page2");
const merged = session.merge(["tab-1", "tab-2"]);
Assert.equal(
merged.lookup("https://example.com/page1"),
"Should return normalized URL from tab-1"
);
Assert.equal(
merged.lookup("https://example.com/page2"),
"Should return normalized URL from tab-2"
);
Assert.equal(merged.size(), 2, "Should have 2 URLs");
});
/**
* Test: removeTab() removes a tab's ledger.
*
* Reason:
* When a tab is closed, its ledger should be removed to free memory.
* Accessing the same tab ID later should create a fresh empty ledger.
*/
add_task(async function test_SessionLedger_removeTab() {
const session = new SessionLedger("session-123");
session.forTab("tab-1").add("https://example.com");
session.forTab("tab-2").add("https://example.com");
session.removeTab("tab-1");
Assert.equal(session.tabCount(), 1, "Should have 1 tab after removal");
// Getting the tab again should create a new empty ledger
const newLedger = session.forTab("tab-1");
Assert.equal(
newLedger.size(),
0,
"New ledger for removed tab should be empty"
);
});
/**
* Test: clearAll() clears all tab ledgers.
*
* Reason:
* Session reset or cleanup may require removing all ledgers at once.
* clearAll() must remove all tabs and their associated ledgers.
*/
add_task(async function test_SessionLedger_clearAll() {
const session = new SessionLedger("session-123");
session.forTab("tab-1").add("https://example.com");
session.forTab("tab-2").add("https://example.com");
session.clearAll();
Assert.equal(session.tabCount(), 0, "Should have no tabs after clearAll");
});
/**
* Test: ledgers normalize URLs consistently.
*
* Reason:
* URLs must be normalized both when added and when checked. A URL
* added with a fragment should match a check without the fragment
* (and vice versa) after normalization.
*/
add_task(async function test_ledger_normalizes_urls() {
const ledger = new TabLedger("tab-123");
// Add URL with fragment
// Check without fragment (should still match after normalization)
Assert.equal(
ledger.lookup("https://example.com/page"),
"Should return normalized URL without fragment"
);
});
/**
* Test: mergeAll returns empty view for empty ledger.
*
* Reason:
* When no tabs have been seeded, mergeAll should return an empty view
* with size() === 0.
*/
add_task(async function test_mergeAll_empty_ledger() {
const sessionLedger = new SessionLedger("session-1");
const result = sessionLedger.mergeAll();
Assert.equal(result.size(), 0, "Empty ledger should return empty view");
});
/**
* Test: mergeAll includes URLs from a single tab.
*
* Reason:
* mergeAll should include all URLs from all tabs. With one tab,
* it should return all URLs from that tab.
*/
add_task(async function test_mergeAll_single_tab() {
const sessionLedger = new SessionLedger("session-1");
sessionLedger.forTab("tab-1").add("https://mozilla.org");
sessionLedger.forTab("tab-1").add("https://example.com");
const result = sessionLedger.mergeAll();
Assert.equal(result.size(), 2, "Should contain both URLs");
// Note: lookup() normalizes URLs internally, so trailing slash is added
Assert.equal(
result.lookup("https://mozilla.org/"),
"Should have mozilla.org"
);
Assert.equal(
result.lookup("https://example.com/"),
"Should have example.com"
);
});
/**
* Test: mergeAll deduplicates URLs across tabs.
*
* Reason:
* The same URL trusted in multiple tabs should only appear once
* in the merged view.
*/
add_task(async function test_mergeAll_multiple_tabs_deduped() {
const sessionLedger = new SessionLedger("session-1");
sessionLedger.forTab("tab-1").add("https://mozilla.org");
sessionLedger.forTab("tab-2").add("https://mozilla.org");
sessionLedger.forTab("tab-2").add("https://github.com");
const result = sessionLedger.mergeAll();
Assert.equal(result.size(), 2, "Duplicate URLs should be deduped");
// Note: lookup() normalizes URLs internally, so trailing slash is added
Assert.equal(
result.lookup("https://mozilla.org/"),
"Should have mozilla.org"
);
Assert.equal(
result.lookup("https://github.com/"),
"Should have github.com"
);
});
/**
* Test: mergeAll returns same API as merge().
*
* Reason:
* mergeAll should return the same type of view as merge() for
* API consistency - an object with has() and size() methods.
*/
add_task(async function test_mergeAll_returns_same_api_as_merge() {
const sessionLedger = new SessionLedger("session-1");
sessionLedger.forTab("tab-1").add("https://mozilla.org");
const mergeAllResult = sessionLedger.mergeAll();
const mergeResult = sessionLedger.merge(["tab-1"]);
Assert.equal(
typeof mergeAllResult.lookup,
"function",
"mergeAll should return object with lookup()"
);
Assert.equal(
typeof mergeResult.lookup,
"function",
"merge should return object with lookup()"
);
Assert.equal(
typeof mergeAllResult.size,
"function",
"mergeAll should return object with size()"
);
Assert.equal(
typeof mergeResult.size,
"function",
"merge should return object with size()"
);
});