Skip to content

trusted types #695

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 1 commit into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions src/browser/html/html.zig
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ const History = @import("history.zig").History;
const Location = @import("location.zig").Location;
const MediaQueryList = @import("media_query_list.zig").MediaQueryList;
const Performance = @import("performance.zig").Performance;
const TrustedTypes = @import("trusted_types.zig");

pub const Interfaces = .{
HTMLDocument,
Expand All @@ -38,4 +39,5 @@ pub const Interfaces = .{
Location,
MediaQueryList,
Performance,
TrustedTypes.Interfaces,
};
149 changes: 149 additions & 0 deletions src/browser/html/trusted_types.zig
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
// Copyright (C) 2023-2025 Lightpanda (Selecy SAS)
//
// Francis Bouvier <[email protected]>
// Pierre Tachoire <[email protected]>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as
// published by the Free Software Foundation, either version 3 of the
// License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.

const std = @import("std");
const Allocator = std.mem.Allocator;

const Env = @import("../env.zig").Env;
const SessionState = @import("../env.zig").SessionState;

const log = std.log.scoped(.trusted_types);

pub const Interfaces = .{
TrustedTypePolicyFactory,
TrustedTypePolicy,
TrustedTypePolicyOptions,
TrustedHTML,
TrustedScript,
TrustedScriptURL,
};

const TrustedHTML = struct {
value: []const u8,

// TODO _toJSON
pub fn _toString(self: *const TrustedHTML) []const u8 {
return self.value;
}
};
const TrustedScript = struct {
value: []const u8,

pub fn _toString(self: *const TrustedScript) []const u8 {
return self.value;
}
};
const TrustedScriptURL = struct {
value: []const u8,

pub fn _toString(self: *const TrustedScriptURL) []const u8 {
return self.value;
}
};

// https://developer.mozilla.org/en-US/docs/Web/API/TrustedTypePolicyFactory
pub const TrustedTypePolicyFactory = struct {
// TBD innerHTML if set the default createHTML should be used when `element.innerHTML = userInput;` does v8 do that for us? Prob not.
default_policy: ?TrustedTypePolicy = null, // The default policy, set by creating a policy with the name "default".
created_policy_names: std.ArrayListUnmanaged([]const u8) = .empty,

pub fn _defaultPolicy(self: *TrustedTypePolicyFactory) ?TrustedTypePolicy {
return self.default_policy;
}

// https://w3c.github.io/trusted-types/dist/spec/#dom-trustedtypepolicyfactory-createpolicy
// https://w3c.github.io/trusted-types/dist/spec/#abstract-opdef-create-a-trusted-type-policy
pub fn _createPolicy(self: *TrustedTypePolicyFactory, name: []const u8, options: ?TrustedTypePolicyOptions, state: *SessionState) !TrustedTypePolicy {
// TODO Throw TypeError if policy names are restricted by the Content Security Policy trusted-types directive and this name is not on the allowlist.
// TODO Throw TypeError if the name is a duplicate and the Content Security Policy trusted-types directive is not using allow-duplicates

const policy = TrustedTypePolicy{
.name = name,
.options = options orelse TrustedTypePolicyOptions{},
};

if (std.mem.eql(u8, name, "default")) {
// TBD what if default_policy is already set?
self.default_policy = policy;
}
try self.created_policy_names.append(state.arena, try state.arena.dupe(u8, name));

return policy;
}
};

pub const TrustedTypePolicyOptions = struct {
createHTML: ?Env.Function = null, // (str, ..args) -> str
createScript: ?Env.Function = null, // (str, ..args) -> str
createScriptURL: ?Env.Function = null, // (str, ..args) -> str
};

// https://developer.mozilla.org/en-US/docs/Web/API/TrustedTypePolicy
pub const TrustedTypePolicy = struct {
name: []const u8,
options: TrustedTypePolicyOptions,

pub fn get_name(self: *TrustedTypePolicy) []const u8 {
return self.name;
}

pub fn _createHTML(self: *TrustedTypePolicy, html: []const u8) !TrustedHTML {
// TODO handle throwIfMissing
const create = self.options.createHTML orelse return error.TypeError;

var result: Env.Function.Result = undefined;
const out = try create.tryCall([]const u8, .{html}, &result); // TODO varargs
return .{
.value = out,
};
}

pub fn _createScript(self: *TrustedTypePolicy, script: []const u8) !TrustedScript {
// TODO handle throwIfMissing
const create = self.options.createScript orelse return error.TypeError;

var result: Env.Function.Result = undefined;
return try create.tryCall(TrustedScript, .{script}, &result); // TODO varargs
}

pub fn _createScriptURL(self: *TrustedTypePolicy, url: []const u8) !TrustedScriptURL {
// TODO handle throwIfMissing
const create = self.options.createScriptURL orelse return error.TypeError;

var result: Env.Function.Result = undefined;
return try create.tryCall(TrustedScriptURL, .{url}, &result); // TODO varargs
}
};

const testing = @import("../../testing.zig");
test "Browser.TrustedTypes" {
var runner = try testing.jsRunner(testing.tracking_allocator, .{});
defer runner.deinit();

try runner.testCases(&.{
.{ "trustedTypes", "[object TrustedTypePolicyFactory]" },
.{
\\ let escapeHTMLPolicy = trustedTypes.createPolicy('myEscapePolicy', {
\\ createHTML: (string) => string.replace(/</g, "&lt;"),
\\ });
,
null,
},
.{ "escapeHTMLPolicy.createHTML('<img src=x onerror=alert(1)>');", "&lt;img src=x onerror=alert(1)>" },
}, .{});
}
6 changes: 6 additions & 0 deletions src/browser/html/window.zig
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ const Console = @import("../console/console.zig").Console;
const EventTarget = @import("../dom/event_target.zig").EventTarget;
const MediaQueryList = @import("media_query_list.zig").MediaQueryList;
const Performance = @import("performance.zig").Performance;
const TrustedTypePolicyFactory = @import("trusted_types.zig").TrustedTypePolicyFactory;

const storage = @import("../storage/storage.zig");

Expand Down Expand Up @@ -58,6 +59,7 @@ pub const Window = struct {
console: Console = .{},
navigator: Navigator = .{},
performance: Performance,
trusted_types: TrustedTypePolicyFactory = .{},

pub fn create(target: ?[]const u8, navigator: ?Navigator) !Window {
var fbs = std.io.fixedBufferStream("");
Expand Down Expand Up @@ -154,6 +156,10 @@ pub const Window = struct {
return &self.performance;
}

pub fn get_trustedTypes(self: *Window) !TrustedTypePolicyFactory {
return self.trusted_types;
}

// Tells the browser you wish to perform an animation. It requests the browser to call a user-supplied callback function before the next repaint.
// fn callback(timestamp: f64)
// Returns the request ID, that uniquely identifies the entry in the callback list.
Expand Down
2 changes: 1 addition & 1 deletion src/runtime/js.zig
Original file line number Diff line number Diff line change
Expand Up @@ -1254,7 +1254,7 @@ pub fn Env(comptime State: type, comptime WebApis: type) type {
return self.tryCallWithThis(T, self.getThis(), args, result);
}

pub fn tryCallWithThis(self: *const Function, comptime T: type, this: anytype, args: anytype, result: *Result) !void {
pub fn tryCallWithThis(self: *const Function, comptime T: type, this: anytype, args: anytype, result: *Result) !T {
var try_catch: TryCatch = undefined;
try_catch.init(self.scope);
defer try_catch.deinit();
Expand Down