Skip to content

Commit 9574528

Browse files
[GENAI-2335] Implement Policy Engine for Smart Window tool execution
Add security layer policy enforcement for Smart Window tool execution with policy to prevent exfiltration links Components: - SecurityOrchestrator: Central coordination with pref switch - PolicyEvaluator/ConditionEvaluator: JSON-based policy evaluation - SecurityUtils: URL normalization and session-scoped ledgers - SmartWindowMeta actors: Page metadata extraction (canonical/og:url) - DecisionTypes: Structured allow/deny decisions Security model: Explicit seeding with deterministic URL validation, fail-closed behavior, and pref switch (browser.smartwindow.security.enabled).
1 parent d29c613 commit 9574528

26 files changed

Lines changed: 5396 additions & 34 deletions
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
/* This Source Code Form is subject to the terms of the Mozilla Public
2+
* License, v. 2.0. If a copy of the MPL was not distributed with this
3+
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
4+
5+
/**
6+
* Content-process actor for extracting page metadata (page URL, canonical, og:url).
7+
* The parent actor validates and normalizes these URLs before seeding the security ledger.
8+
*/
9+
export class SmartWindowMetaChild extends JSWindowActorChild {
10+
/**
11+
* Receives queries from the parent process.
12+
*
13+
* @param {ReceiveMessageArgument} message - The message from parent
14+
* @returns {Promise<object>} Metadata object with URLs
15+
*/
16+
receiveMessage(message) {
17+
switch (message.name) {
18+
case "SmartWindowMeta:GetMetadata":
19+
return this.getMetadata();
20+
default:
21+
return Promise.reject(new Error(`Unknown message: ${message.name}`));
22+
}
23+
}
24+
25+
/**
26+
* Extracts metadata from the current page.
27+
*
28+
* @returns {object} Metadata with pageUrl, canonical, and ogUrl (raw strings)
29+
*/
30+
getMetadata() {
31+
const doc = this.contentWindow?.document;
32+
33+
if (!doc) {
34+
return { pageUrl: "", canonical: "", ogUrl: "" };
35+
}
36+
37+
const pageUrl = doc.location?.href || "";
38+
39+
let canonical = "";
40+
try {
41+
const canonicalLink = doc.querySelector('link[rel="canonical"]');
42+
canonical = canonicalLink?.getAttribute("href") || "";
43+
} catch {
44+
// querySelector may fail on some documents (e.g., XML)
45+
}
46+
47+
let ogUrl = "";
48+
try {
49+
const ogUrlMeta = doc.querySelector('meta[property="og:url"]');
50+
ogUrl = ogUrlMeta?.getAttribute("content") || "";
51+
} catch {
52+
// querySelector may fail on some documents
53+
}
54+
55+
return { pageUrl, canonical, ogUrl };
56+
}
57+
}
Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
/* This Source Code Form is subject to the terms of the Mozilla Public
2+
* License, v. 2.0. If a copy of the MPL was not distributed with this
3+
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
4+
5+
import {
6+
normalizeUrl,
7+
isSameETLDPlusOne,
8+
} from "chrome://global/content/ml/security/SecurityUtils.sys.mjs";
9+
10+
/**
11+
* Chrome-process actor for validating page metadata and seeding security ledger.
12+
* Validates canonical/og:url have same eTLD+1 as page URL before seeding.
13+
*/
14+
export class SmartWindowMetaParent extends JSWindowActorParent {
15+
/**
16+
* Seeds the security ledger for the given browser/tab.
17+
*
18+
* @param {object} sessionLedger - The SessionLedger instance
19+
* @param {string} tabId - The tab identifier (typically linkedPanel)
20+
* @returns {Promise<object>} Result with seededUrls, skippedUrls, and errors
21+
*/
22+
async seedLedgerForTab(sessionLedger, tabId) {
23+
const result = {
24+
success: false,
25+
seededUrls: [],
26+
skippedUrls: [],
27+
errors: [],
28+
};
29+
30+
try {
31+
const metadata = await this.sendQuery("SmartWindowMeta:GetMetadata");
32+
33+
if (!metadata || !metadata.pageUrl) {
34+
result.errors.push("No page URL available from content process");
35+
return result;
36+
}
37+
38+
const { pageUrl, canonical, ogUrl } = metadata;
39+
40+
const normalizedPageUrl = normalizeUrl(pageUrl);
41+
if (!normalizedPageUrl.success) {
42+
result.errors.push({
43+
url: pageUrl,
44+
reason: "Page URL normalization failed",
45+
error: normalizedPageUrl.error,
46+
});
47+
return result;
48+
}
49+
50+
const urlsToSeed = [normalizedPageUrl.url];
51+
result.seededUrls.push({
52+
original: pageUrl,
53+
normalized: normalizedPageUrl.url,
54+
source: "page",
55+
});
56+
57+
if (canonical) {
58+
const validated = this.#validateSecondaryUrl(
59+
canonical,
60+
normalizedPageUrl.url,
61+
pageUrl,
62+
"canonical"
63+
);
64+
65+
if (validated.success) {
66+
urlsToSeed.push(validated.normalizedUrl);
67+
result.seededUrls.push({
68+
original: canonical,
69+
normalized: validated.normalizedUrl,
70+
source: "canonical",
71+
});
72+
} else {
73+
result.skippedUrls.push({
74+
original: canonical,
75+
source: "canonical",
76+
reason: validated.reason,
77+
});
78+
}
79+
}
80+
81+
if (ogUrl) {
82+
const validated = this.#validateSecondaryUrl(
83+
ogUrl,
84+
normalizedPageUrl.url,
85+
pageUrl,
86+
"og:url"
87+
);
88+
89+
if (validated.success) {
90+
urlsToSeed.push(validated.normalizedUrl);
91+
result.seededUrls.push({
92+
original: ogUrl,
93+
normalized: validated.normalizedUrl,
94+
source: "og:url",
95+
});
96+
} else {
97+
result.skippedUrls.push({
98+
original: ogUrl,
99+
source: "og:url",
100+
reason: validated.reason,
101+
});
102+
}
103+
}
104+
105+
sessionLedger.forTab(tabId).seed(urlsToSeed, pageUrl);
106+
result.success = true;
107+
} catch (error) {
108+
result.errors.push({
109+
message: "Actor communication failed",
110+
error: error.message || String(error),
111+
});
112+
}
113+
114+
return result;
115+
}
116+
117+
/**
118+
* Validates a secondary URL (canonical or og:url) against the page's eTLD+1.
119+
*
120+
* @param {string} url - The URL to validate (may be relative)
121+
* @param {string} normalizedPageUrl - The normalized page URL for eTLD+1 comparison
122+
* @param {string} baseUrl - The original page URL for resolving relative URLs
123+
* @param {string} source - Source identifier ("canonical" or "og:url")
124+
* @returns {object} Validation result with success flag and normalizedUrl or reason
125+
* @private
126+
*/
127+
#validateSecondaryUrl(url, normalizedPageUrl, baseUrl, source) {
128+
const normalized = normalizeUrl(url, baseUrl);
129+
130+
if (!normalized.success) {
131+
return {
132+
success: false,
133+
reason: "Normalization failed",
134+
details: normalized.error,
135+
};
136+
}
137+
138+
if (!isSameETLDPlusOne(normalizedPageUrl, normalized.url)) {
139+
return {
140+
success: false,
141+
reason: "Different eTLD+1 from page URL",
142+
pageUrl: normalizedPageUrl,
143+
secondaryUrl: normalized.url,
144+
};
145+
}
146+
147+
return { success: true, normalizedUrl: normalized.url };
148+
}
149+
}

browser/components/smartwindow/content/smartwindow.mjs

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,11 +31,36 @@ const { SessionStore } = ChromeUtils.importESModule(
3131
const { TabStateFlusher } = ChromeUtils.importESModule(
3232
"resource:///modules/sessionstore/TabStateFlusher.sys.mjs"
3333
);
34+
const { SecurityOrchestrator } = ChromeUtils.importESModule(
35+
"chrome://global/content/ml/security/SecurityOrchestrator.sys.mjs"
36+
);
3437
const { embedderElement, topChromeWindow } = window.browsingContext;
3538
const gBrowser = topChromeWindow.gBrowser;
3639

3740
const FIRST_RUN_PREF = "browser.smartwindow.firstrun.didSeeWelcome";
3841

42+
// Register SmartWindowMeta actor for secure page metadata extraction
43+
try {
44+
ChromeUtils.registerWindowActor("SmartWindowMeta", {
45+
parent: {
46+
esModuleURI: "chrome://browser/content/smartwindow/actors/SmartWindowMetaParent.sys.mjs",
47+
},
48+
child: {
49+
esModuleURI: "chrome://browser/content/smartwindow/actors/SmartWindowMetaChild.sys.mjs",
50+
events: {
51+
DOMContentLoaded: {},
52+
},
53+
},
54+
allFrames: true,
55+
});
56+
} catch (e) {
57+
// Actor already registered - this is expected if Smart Window
58+
// has been opened before in this browser session
59+
if (!e.message?.includes("already registered")) {
60+
console.error("Failed to register SmartWindowMeta actor:", e);
61+
}
62+
}
63+
3964
/**
4065
*
4166
*/
@@ -88,6 +113,25 @@ class SmartWindowPage {
88113

89114
this.#chatHistory = new ChatHistory();
90115

116+
// Initialize security orchestrator (if enabled)
117+
// Creates the SessionLedger that tracks trusted URLs across tabs
118+
// Only initialize if Smart Window security is enabled
119+
const sessionId = `smart-window-${Date.now()}-${Math.random().toString(36).slice(2, 11)}`;
120+
const securityEnabled = Services.prefs.getBoolPref("browser.smartwindow.security.enabled", true);
121+
122+
if (securityEnabled) {
123+
SecurityOrchestrator.init(sessionId).then(sessionLedger => {
124+
this.sessionLedger = sessionLedger;
125+
console.log("[Security] Smart Window security enabled - orchestrator initialized");
126+
}).catch(error => {
127+
console.error("[Security] Failed to initialize orchestrator:", error);
128+
this.sessionLedger = null;
129+
});
130+
} else {
131+
this.sessionLedger = null;
132+
console.log("[Security] Smart Window security DISABLED via kill switch - running in pass-through mode");
133+
}
134+
91135
gBrowser.selectedTab.conversation = new ChatHistoryConversation({
92136
title: "",
93137
description: "",
@@ -134,6 +178,8 @@ class SmartWindowPage {
134178
this.onboardingPrefObserver
135179
);
136180
}
181+
182+
SecurityOrchestrator.reset();
137183
}
138184

139185
getQueryTypeIcon(type) {

0 commit comments

Comments
 (0)