LibWeb/Bindings: Implement [SecureContext] extended attribute

Unfortunately this is a bit of a pain to test as it is surprisingly
difficult to create a non secure context in our test harness.
This is because both file scheme URLs and localhost are considered
secure contexts.

To test this, add a very specific internals setter to change the
top level origin of the environment for the current realm.
This commit is contained in:
Shannon Booth
2026-01-18 17:27:18 +01:00
committed by Shannon Booth
parent 6bdced0014
commit bc93ba4530
Notes: github-actions[bot] 2026-02-14 19:35:46 +00:00
8 changed files with 117 additions and 3 deletions

View File

@@ -2,7 +2,7 @@
* Copyright (c) 2022, Andrew Kaster <akaster@serenityos.org>
* Copyright (c) 2023, Linus Groh <linusg@serenityos.org>
* Copyright (c) 2023, Luke Wilde <lukew@serenityos.org>
* Copyright (c) 2025, Shannon Booth <shannon@serenityos.org>
* Copyright (c) 2025-2026, Shannon Booth <shannon@serenityos.org>
*
* SPDX-License-Identifier: BSD-2-Clause
*/

View File

@@ -9,6 +9,7 @@
#include <LibGfx/Cursor.h>
#include <LibJS/Runtime/Date.h>
#include <LibJS/Runtime/VM.h>
#include <LibURL/Parser.h>
#include <LibUnicode/TimeZone.h>
#include <LibWeb/ARIA/AriaData.h>
#include <LibWeb/ARIA/StateAndProperties.h>
@@ -532,4 +533,10 @@ void Internals::clear_element(HTML::HTMLElement& element)
form_associated_element.clear_algorithm();
}
void Internals::set_environments_top_level_url(StringView url)
{
auto& realm = *vm().current_realm();
HTML::principal_realm_settings_object(realm).top_level_creation_url = URL::Parser::basic_parse(url);
}
}

View File

@@ -105,6 +105,7 @@ public:
void set_highlighted_node(GC::Ptr<DOM::Node> node);
void clear_element(HTML::HTMLElement&);
void set_environments_top_level_url(StringView url);
private:
explicit Internals(JS::Realm&);

View File

@@ -79,6 +79,8 @@ interface Internals {
undefined handleSDLInputEvents();
undefined setEnvironmentsTopLevelURL(USVString url);
InternalGamepad connectVirtualGamepad();
undefined setHighlightedNode(Node? node);

View File

@@ -3731,11 +3731,21 @@ void @class_name@::initialize(JS::Realm& realm)
continue;
auto attribute_generator = generator_for_member(attribute.name, attribute.extended_attributes);
if (attribute.extended_attributes.contains("SecureContext")) {
attribute_generator.append(R"~~~(
if (HTML::is_secure_context(Bindings::principal_host_defined_environment_settings_object(realm))) {)~~~");
}
if (attribute.extended_attributes.contains("FIXME")) {
attribute_generator.set("attribute.name", attribute.name);
attribute_generator.append(R"~~~(
@define_direct_property@("@attribute.name@"_utf16_fly_string, JS::js_undefined(), default_attributes | JS::Attribute::Unimplemented);
)~~~");
if (attribute.extended_attributes.contains("SecureContext")) {
attribute_generator.append(R"~~~(
})~~~");
}
continue;
}
@@ -3778,6 +3788,11 @@ void @class_name@::initialize(JS::Realm& realm)
attribute_generator.append(R"~~~(
@define_direct_accessor@("@attribute.name@"_utf16_fly_string, native_@attribute.getter_callback@, native_@attribute.setter_callback@, default_attributes);
)~~~");
if (attribute.extended_attributes.contains("SecureContext")) {
attribute_generator.append(R"~~~(
})~~~");
}
}
for (auto& function : interface.functions) {
@@ -3823,6 +3838,11 @@ void @class_name@::initialize(JS::Realm& realm)
function_generator.set("function.name:snakecase", make_input_acceptable_cpp(overload_set.key.to_snakecase()));
function_generator.set("function.length", ByteString::number(get_shortest_function_length(overload_set.value)));
if (function.extended_attributes.contains("SecureContext")) {
function_generator.append(R"~~~(
if (HTML::is_secure_context(Bindings::principal_host_defined_environment_settings_object(realm))) {)~~~");
}
if (any_of(overload_set.value, [](auto const& function) { return function.extended_attributes.contains("Unscopable"); })) {
VERIFY(all_of(overload_set.value, [](auto const& function) { return function.extended_attributes.contains("Unscopable"); }));
function_generator.append(R"~~~(
@@ -3833,6 +3853,11 @@ void @class_name@::initialize(JS::Realm& realm)
function_generator.append(R"~~~(
@define_native_function@(realm, "@function.name@"_utf16_fly_string, @function.name:snakecase@, @function.length@, default_attributes);
)~~~");
if (function.extended_attributes.contains("SecureContext")) {
function_generator.append(R"~~~(
})~~~");
}
}
bool should_generate_stringifier = true;
@@ -5548,6 +5573,7 @@ void generate_prototype_implementation(IDL::Interface const& interface, StringBu
#include <LibURL/Origin.h>
#include <LibWeb/Bindings/@prototype_class@.h>
#include <LibWeb/Bindings/ExceptionOrUtils.h>
#include <LibWeb/Bindings/PrincipalHostDefined.h>
#include <LibWeb/Bindings/Intrinsics.h>
#include <LibWeb/DOM/Element.h>
#include <LibWeb/DOM/Event.h>
@@ -5969,6 +5995,7 @@ void generate_global_mixin_implementation(IDL::Interface const& interface, Strin
#include <LibWeb/Bindings/@prototype_name@.h>
#include <LibWeb/Bindings/ExceptionOrUtils.h>
#include <LibWeb/Bindings/Intrinsics.h>
#include <LibWeb/Bindings/PrincipalHostDefined.h>
#include <LibWeb/DOM/Element.h>
#include <LibWeb/DOM/Event.h>
#include <LibWeb/DOM/IDLEventListener.h>

View File

@@ -120,6 +120,7 @@ static ErrorOr<void> generate_intrinsic_definitions_implementation(StringView ou
#include <LibGC/DeferGC.h>
#include <LibJS/Runtime/Object.h>
#include <LibWeb/Bindings/Intrinsics.h>
#include <LibWeb/Bindings/PrincipalHostDefined.h>
#include <LibWeb/Export.h>
#include <LibWeb/HTML/Window.h>
#include <LibWeb/HTML/DedicatedWorkerGlobalScope.h>
@@ -182,6 +183,27 @@ void Intrinsics::create_web_namespace<@namespace_class@>(JS::Realm& realm)
)~~~");
};
generator.append(R"~~~(
static bool is_secure_context_interface(InterfaceName name)
{
switch (name) {
)~~~");
for (auto const& interface : interface_sets.intrinsics) {
if (!interface.extended_attributes.contains("SecureContext"))
continue;
generator.set("secure_context_interface_name", interface.name);
generator.append(R"~~~(
case InterfaceName::@secure_context_interface_name@:)~~~");
}
generator.append(R"~~~(
return true;
default:
return false;
}
}
)~~~");
auto generate_global_exposed = [&generator](StringView global_name, Vector<IDL::Interface&> const& interface_set) {
generator.set("global_name", global_name);
generator.append(R"~~~(
@@ -235,8 +257,11 @@ bool is_exposed(InterfaceName name, JS::Realm& realm)
TODO(); // FIXME: ServiceWorkerGlobalScope and WorkletGlobalScope.
}
// FIXME: 2. If realms settings object is not a secure context, and construct is conditionally exposed on
// [SecureContext], then return false.
// 2. If realms settings object is not a secure context, and construct is conditionally exposed on
// [SecureContext], then return false.
if (is_secure_context_interface(name) && HTML::is_non_secure_context(principal_host_defined_environment_settings_object(realm)))
return false;
// FIXME: 3. If realms settings objects cross-origin isolated capability is false, and construct is
// conditionally exposed on [CrossOriginIsolated], then return false.
@@ -352,6 +377,7 @@ static ErrorOr<void> generate_exposed_interface_implementation(StringView class_
#include <LibJS/Runtime/Object.h>
#include <LibWeb/Bindings/Intrinsics.h>
#include <LibWeb/Bindings/@global_object_name@ExposedInterfaces.h>
#include <LibWeb/HTML/Scripting/Environments.h>
)~~~");
for (auto& interface : exposed_interfaces) {
auto gen = generator.fork();
@@ -382,6 +408,7 @@ namespace Web::Bindings {
void add_@global_object_snake_name@_exposed_interfaces(JS::Object& global)
{
static constexpr u8 attr = JS::Attribute::Writable | JS::Attribute::Configurable;
[[maybe_unused]] bool is_secure_context = HTML::is_secure_context(HTML::relevant_principal_settings_object(global));
)~~~");
auto add_interface = [class_name](SourceGenerator& gen, IDL::Interface const& interface) {
@@ -393,6 +420,11 @@ void add_@global_object_snake_name@_exposed_interfaces(JS::Object& global)
gen.set("interface_name", interface.namespaced_name);
gen.set("prototype_class", interface.prototype_class);
if (interface.extended_attributes.contains("SecureContext")) {
gen.append(R"~~~(
if (is_secure_context) {)~~~");
}
gen.append(R"~~~(
global.define_intrinsic_accessor("@interface_name@"_utf16_fly_string, attr, [](auto& realm) -> JS::Value { return &ensure_web_constructor<@prototype_class@>(realm, "@interface_name@"_fly_string); });)~~~");
@@ -417,6 +449,11 @@ void add_@global_object_snake_name@_exposed_interfaces(JS::Object& global)
gen.append(R"~~~(
global.define_intrinsic_accessor("@legacy_interface_name@"_utf16_fly_string, attr, [](auto& realm) -> JS::Value { return &ensure_web_constructor<@prototype_class@>(realm, "@legacy_interface_name@"_fly_string); });)~~~");
}
if (interface.extended_attributes.contains("SecureContext")) {
gen.append(R"~~~(
})~~~");
}
};
auto add_namespace = [](SourceGenerator& gen, StringView name, StringView namespace_class) {

View File

@@ -0,0 +1,5 @@
{
"SubtleCrypto": false,
"Clipboard": false,
"CookieChangeEvent": true
}

View File

@@ -0,0 +1,35 @@
<!DOCTYPE html>
<script src="include.js"></script>
<script>
asyncTest(async (done) => {
const httpServer = httpTestServer();
const url = await httpServer.createEcho("GET", "/secure-context-idl", {
status: 200,
headers: {
"Access-Control-Allow-Origin": "*",
"Content-Type": "text/html",
},
body: `
<script>
// HTTP is not a secure context.
internals.setEnvironmentsTopLevelURL('http://ladybird.org');
parent.postMessage({
SubtleCrypto: !!crypto?.subtle,
Clipboard: typeof navigator.clipboard === "object",
CookieChangeEvent: typeof CookieChangeEvent === "function",
}, "*");
<\/script>`,
});
const frame = document.createElement('iframe');
frame.src = url;
addEventListener("message", (event) => {
println(JSON.stringify(event.data, null, 2));
done();
}, false);
document.body.appendChild(frame);
});
</script>