Skip to content
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

Support configurable hooking into LavaMoat scuttling #462

Merged
merged 22 commits into from
Jun 14, 2023

Conversation

weizman
Copy link
Member

@weizman weizman commented Mar 2, 2023

Motivation

⚠️ EXPERIMENTAL - API might change in future versions!

We'd like to be able to optionally hook into the scuttling process at runtime.
It will be useful to us because it'll allow us to use Snow to perform scuttling on all new realms instead of just the main one.

This continues the work of #360 where scuttling was originally introduced

Implementation

before:

scuttleGlobalThis: <boolean>
scuttleGlobalThisExceptions: []<string>

after:

scuttleGlobalThis: {
  enabled: <boolean>,
  exceptions: []<string>,
  scuttler: <string>, // e.g. 'SCUTTLER'
}

The new option scuttler will be a string pointing to a reference on the global object, which should refer to a function:

Object.defineProperty(window, 'SCUTTLER', {
        value: (globalRef, scuttle) => {
          console.log('scuttler (start)');
          scuttle(globalRef);
          console.log('scuttler (end)');
        }
})

When scuttler is left untouched, LavaMoat runtime won't try using an external scuttler.

Visit scuttle docs for further info

@weizman weizman marked this pull request as ready for review March 9, 2023 09:55
@weizman weizman marked this pull request as draft March 9, 2023 10:01
@weizman
Copy link
Member Author

weizman commented Mar 15, 2023

Intro

Thinking out loud on how configuration should look like.

There are a few things to be considered when deciding on the right configuration for the motivation this PR pushes forward:

  • It needs to be easy for the user to understand.
  • The idea is to be able to configure whether we want Snow to automatically ship scuttling security feature to all potential child realms in the browser LavaMoat build or not.
  • In the future, Snow might be used for more stuff than just scuttling.
  • In the future, Snow will still only serve the browser-based LavaMoat builds.

Option 1

We could turn scuttleGlobalThis into a multi-value option to represent the 3 possible states:

  • 'none'[:default] - do not activate scuttling.
  • 'top-realm' - activate scuttling for the top main realm only.
  • 'all-realms' - activate scuttling through Snow so it will apply scuttling to all potential child realms automatically.

Few thoughts:

  • Could use some help with the naming of the fields.
  • This doesn't answer our future potential need where we'll likely be using Snow for other stuff than just scuttling.
  • A bit messy?
  • 'all-realms' is irrelevant in non-browser-based envs.

Option 2

We could just implement the activation of Snow as a separate config field, such as useSnow:

  • false[:default] - don't use Snow for anything.
  • true - whenever Snow can be helpful for shipping security to child realms (such as - but not limited to - scuttling), use it.
    • Unless such certain security feature is also configurable and is turned off.

Few thoughts:

  • Could use some help with the naming of the field.
  • This introduces yet another config field, although boolean is less messy.

Option 3

Any other suggestions? Would love to hear them.

@naugtur
Copy link
Member

naugtur commented Mar 15, 2023

In the future, Snow might be used for more stuff than just scuttling.
It will still be used as a mechanism to deliver changes to other realms transparently and the user should not need exposure to the word "snow" in the config. So no option2

In the future, Snow will still only serve the browser-based LavaMoat builds.
This means we want to make it less likely a Node.js user would attempt to use a feature that's irrelevant for them

For option1:

scuttleGlobalThis: [ 'no', 'yes', 'all-browser-realms' ]

But to address the browser being separate, we could do something like this:

scuttleGlobalThis: boolean,
protectBrowserRealms: [ 'scuttle', 'ban' ]

with 'scuttle' being the only option for now, but open for future expansion

@legobeat
Copy link
Collaborator

legobeat commented Mar 16, 2023

protectBrowserRealms: [ 'scuttle', 'ban' ]

@naugtur why ban?

@weizman In general I agree with the point raised by @naugtur to not go for 2.

If I understand it right, I'm thinking of this as if/how recursion should be done.
I also think it can make sense to make the first-level value into a dict (for extensibility without polluting the options dict).
Putting that together along the line of Option 1:

// 3.1
{
   scuttleGlobalThis: {  // you could optionally recognize a bool here, with `true` being equal to whatever defaults make sense
    enabled: bool,
    exceptions: string[], // both this and existing field would initially be recognized, dropping the old one in a future major release
    recursion: 'full' | 'none', // don't know if it would be useful to have a `number` for "max recursion depth"?
  }
}

if we want to be even more general and not fully close the door on globalThis behavior:

// 3.2
{
   scuttleGlobals: {
    enabled: string[], // default ['globalThis']
    exceptions: string[], // both this and existing field would initially be recognized, dropping the old one in a future major release
    recursion: 'full' | 'none', // don't know if it would be useful to have a `number` for "max recursion depth"?
  }
}

or even

// 3.3
{
   scuttling: {
    globalThis: { // default; keys correspond to globals
      exceptions: string[], // fall back to globalThisExceptions
      recursion: 'full' | 'none', // don't know if it would be useful to have a `number` for "max recursion depth"?
    },
    // , window: {...} etc
  }
}

Going further: Inverting it (configuring options - in this case only scuttling - per global). domain-driven rather than feature-driven configuration:

// 3.4
{
  globals: {
    globalThis: {
      scuttling: {
        enabled: bool,
        exceptions: string[], // fall back to globalThisExceptions
        recursion: 'full' | 'none', // don't know if it would be useful to have a `number` for "max recursion depth"?
    },
    // , window: {...} etc
  }
}

Some variation of any of these seems sensible to me. It's possible that 3.3-3.4 are "overkill" and solving for unneeded granularity (opening for treating different globals differently)

packages/node/src/cli.js Outdated Show resolved Hide resolved
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

once #471 is merged, the diff in this file can be fixed back to make more sense

@weizman weizman changed the title Try integrating snow with scuttling Support LavaMoat scuttling to all realms (with Snow) Mar 21, 2023
@weizman weizman marked this pull request as ready for review March 21, 2023 09:26
@@ -41,16 +41,16 @@ function generateKernel (opts = {}) {
if (opts.hasOwnProperty('scuttleGlobalThis')) {
// scuttleGlobalThis config placeholder should be set only if ordered so explicitly.
// if not, should be left as is to be replaced by a later processor (e.g. LavaPack).
const {scuttleGlobalThis, scuttleGlobalThisExceptions} = opts
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The removal of opts.scuttleGlobalThisExceptions seems to be the only breaking part in this changeset - if it would fall back to the old field in absence of the new one, we should be able to make this non-breaking?

Looks great otherwise!

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you mean on core or in all packages affected in this PR?

Copy link
Collaborator

@legobeat legobeat Mar 21, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All, given that the latest published release of each package exposes the option in the public API.

(In principle of course we can still consider each indepently if there would be particularities to any individual package)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Aren't those going to be breaking changes anyway? considering the scuttleGlobalThis option has changed

Copy link
Collaborator

@legobeat legobeat Mar 21, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The semantics don't seem to have changed? What I mean is that it would still recognize the old config, making that non-breaking (to be actually broken in a future major). Something like:

    if (opts.scuttleGlobalThisExceptions) {
      // log deprecation warning
    }
    const scuttleGlobalThis = opts.scuttleGlobalThis
    const scuttleGlobalThisExceptions = scuttleGlobalThis?.exceptions || opts.scuttleGlobalThisExceptions || []

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can avoid a breaking change here, yes. It's a little ugly in code. It'd be best to not have to do that and not have to bump major. That's what marking a feature as experimental initially is great - we can make breaking changes to it and get away with it.

This feature was never documented. That means, if we document it as experimental, we could consider breaking it knowing that there was no usage beyond what we control in MetaMask. Is this a good idea? Remains to be decided.

In general, we need to set some rules. I suggest:

  • Features or changes cannot be merged without complete documentation.
  • A new feature, distinct from other features, should be marked as experimental initially unless a good reason not to do that is provided.
  • Experimental feature must be listed as experimental in documentation but can remain undocumented in detail.

Copy link
Collaborator

@legobeat legobeat Mar 22, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In general +1 @naugtur but it might be helpful to put the emphasis on "new API or behavior" rather than "new feature" since the former is less open to interpretation (though I'm sure it's arguable that one follows the other)

@@ -1,10 +1,11 @@
<!DOCTYPE HTML>
<html lang="en">
<head>
<script src="https://cdn.jsdelivr.net/npm/@lavamoat/snow/snow.js"></script>
Copy link
Collaborator

@legobeat legobeat Mar 27, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we include (vendor) snow.js inside this directory (or at appropriate place in the tree) to avoid loading from CDN here?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Including it will mean replacing it every time Snow updates. Or, bake it into the HTML, but that will introduce a step in the build process of the example.
We can also just demonstrate the example without Snow (after all - it is optional): 0f7a43d

Comment on lines 44 to 49
const scuttleGlobalThis = opts.scuttleGlobalThis
if (opts.scuttleGlobalThisExceptions) {
console.warn('Lavamoat - "scuttleGlobalThisExceptions" is deprecated. Use "scuttleGlobalThis.exceptions" instead.')
}
const exceptions = scuttleGlobalThis?.exceptions || opts.scuttleGlobalThisExceptions
scuttleGlobalThis.exceptions = exceptions
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
const scuttleGlobalThis = opts.scuttleGlobalThis
if (opts.scuttleGlobalThisExceptions) {
console.warn('Lavamoat - "scuttleGlobalThisExceptions" is deprecated. Use "scuttleGlobalThis.exceptions" instead.')
}
const exceptions = scuttleGlobalThis?.exceptions || opts.scuttleGlobalThisExceptions
scuttleGlobalThis.exceptions = exceptions
if (opts.scuttleGlobalThisExceptions) {
console.warn('Lavamoat - "scuttleGlobalThisExceptions" is deprecated. Use "scuttleGlobalThis.exceptions" instead.')
}
const scuttleGlobalThis = {
...opts.scuttleGlobalThis,
exceptions: opts.scuttleGlobalThis?.exceptions || opts.scuttleGlobalThisExceptions,
}
const exceptions = scuttleGlobalThis.exceptions

This way we leave the opts object unmodified.
I also personally find it more readable to assign all the options fields ni one statement like this, but that might be subjective.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

}
scuttleGlobalThis.exceptions = scuttleGlobalThis?.exceptions || scuttleGlobalThisExceptions
Copy link
Collaborator

@legobeat legobeat Mar 27, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should avoid modifying the input arguments (in this case scuttleGlobalThis.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

for (let i = 0; i < scenario.opts.scuttleGlobalThisExceptions.length; i++) {
scenario.opts.scuttleGlobalThisExceptions[i] = String(scenario.opts.scuttleGlobalThisExceptions[i])
for (let i = 0; i < exceptions.length; i++) {
exceptions[i] = String(exceptions[i])
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is modifying the input (it's desirable to be pure wrt input arguments)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

for (let i = 0; i < scuttleGlobalThisExceptions.length; i++) {
scuttleGlobalThisExceptions[i] = String(scuttleGlobalThisExceptions[i])
for (let i = 0; i < exceptions.length; i++) {
exceptions[i] = String(exceptions[i])
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Follow-up: #597

@weizman weizman changed the title Support LavaMoat scuttling to all realms (with Snow) Support configurable hooking into LavaMoat scuttling Mar 28, 2023
@weilun0510
Copy link

Object.defineProperty(window, 'SCUTTLER', {
        value: (globalRef, scuttle) => {
          console.log('scuttler (start)');
          scuttle(globalRef);
          console.log('scuttler (end)');
        }
})

I don't understand too much, is this the solution?

Where should the following passage be executed?

Object.defineProperty(window, 'SCUTTLER', {
        value: (globalRef, scuttle) => {
          console.log('scuttler (start)');
          scuttle(globalRef);
          console.log('scuttler (end)');
        }
})

@legobeat
Copy link
Collaborator

legobeat commented May 24, 2023

@weizman What do you say about rebasing this one on main (there are conflicts to be resolved) and then merging #475 into here as-is? As you're the author of both and #475 is in the desired direction in any case.

@weizman
Copy link
Member Author

weizman commented May 24, 2023

@legobeat i would love to, but let's first merge the easier ones #482 and #477 into #475 to handle less PRs

@weizman
Copy link
Member Author

weizman commented May 24, 2023

you got it right @weilun0510 - it should be executed at any point before the execution of LavaMoat, so when LavaMoat loads it'll pick up this scuttler option from the global object.

@naugtur
Copy link
Member

naugtur commented Jun 1, 2023

We took a bit too long on this one. Can we ship this in the next release cycle and finish #597 for the release after that?
We've got people waiting for this.

Are there any parts of #597 that are stable and finished that could be moved into this one? We still have time until the current release cycle is done. It'd be best to release node 18 support fully before this.

@weizman
Copy link
Member Author

weizman commented Jun 4, 2023

#597 is not ready to be merged, and picking specifics from it will delay us further, I am against.
These types of things should be reflected after this PR is merged.
I'd appreciate an approve on this PR @naugtur.

Copy link
Member

@naugtur naugtur left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

After in-person conversations I see consensus that this should have been merged a month ago and while we're good at coming up with what to improve, we need to get better at deciding when.

@weizman weizman merged commit f204e84 into main Jun 14, 2023
@weizman weizman deleted the snow-scuttling-integration branch June 14, 2023 08:13
weizman added a commit that referenced this pull request Jun 22, 2023
- integrate with snow (#462)
@weizman weizman mentioned this pull request Jun 22, 2023
weizman added a commit that referenced this pull request Jun 22, 2023
- integrate with snow (#462)
boneskull pushed a commit to boneskull/LavaMoat that referenced this pull request Feb 6, 2024
- integrate with snow (LavaMoat#462)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants