-
Notifications
You must be signed in to change notification settings - Fork 392
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
feat: introduce secure API for template verification #693
Conversation
afterEach(() => (secure.enabled = false)); | ||
|
||
it('forbidden access to template', () => { | ||
function html($api) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you add a comment to explain that you are not using the inline template compiler on purpose. I am in the process of removing all those handcrafting templates.
verifyTemplate | ||
}; | ||
|
||
function registerTemplate(tmpl) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Add missing Template
type.
} | ||
} | ||
|
||
function verifyTemplate(tmpl) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Add missing Template
type.
SECURE_IMPORT_NAME, SECURE_REGISTER_TEMPLATE_METHOD_NAME, | ||
LWC_MODULE_NAME | ||
} from '../../shared/constants'; | ||
import { FunctionDeclaration, ExportDefaultDeclaration } from 'babel-types'; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Use t.FunctionDeclaration
imported line 1 instead of importing the types using named imports.
@@ -1,3 +1,4 @@ | |||
import { ResolvedConfig } from '../config'; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Move relative imports below module imports.
const metadata = generateTemplateMetadata(state); | ||
let templateBody: Array<FunctionDeclaration | ExportDefaultDeclaration> = [t.exportDefaultDeclaration(templateFn)]; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Performance wise, a if/else statement is better than overriding the default value with a single if block.
@@ -17,6 +17,31 @@ function functionMatchCode(fn, code) { | |||
); | |||
} | |||
|
|||
|
|||
describe('option secure', () => { | |||
it.only('validate secure transformation', () => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Remove only
describe('option secure', () => { | ||
it.only('validate secure transformation', () => { | ||
const { code } = compiler(`<template><x-test></x-test></template>`, { secure: true }); | ||
functionMatchCode(code, ` |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Use a fixture for this.
@@ -93,7 +95,7 @@ export function invokeComponentRenderMethod(vm: VM): VNodes { | |||
try { | |||
const html = callHook(component, render); | |||
if (isFunction(html)) { | |||
result = evaluateTemplate(vm, html); | |||
result = evaluateTemplate(vm, secure.verifyTemplate(html)); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The validation should be moved inside the evaluateTemplate
method. The invokeComponentRenderMethod
is invoked for each render, we should only validate the template is the template is different than the previous render.
Benchmark resultsBase commit: |
Benchmark resultsBase commit: lwc-engine-benchmark
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
- see comment regarding setting secure option as true by default
- pls add a test to options.spec.ts to ensure 'secure' option is correctly normalized to default value.
@@ -16,7 +16,8 @@ const DEFAULT_OUTPUT_CONFIG = { | |||
NODE_ENV: "development" | |||
}, | |||
minify: false, | |||
compat: false | |||
compat: false, | |||
secure: false |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I propose making secure value true by default. In my opinion its better to be restrictive and allow open source consumers to opt-out if they like. Additionally, having secure true by default will minimize bugs in aura because it has multiple entry points into compiler invocation which can be more error prone - for example forgetting to pass in 'secure' or creating new compiler entry point without secure will result in inconsistent behavior. I feel like aura should have no control of this flag at all as secure should be enforced for internal dev at all time. Thoughts?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
we don't need an option, it should always be the case.
|
||
it('forbidden access to template', () => { | ||
// We can't use the inline template compiler here | ||
// since precesily we are trying to test that handcrafted |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
typo: precisely instead of precesily
return tmpl; | ||
} | ||
|
||
export const secure = SECURE_OBJECT; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
should we expose a mechanism to de-register the template?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No need.
@@ -97,6 +98,10 @@ export function evaluateTemplate(vm: VM, html: Template): Array<VNode|null> { | |||
// template, because they could have similar IDs, and snabbdom just rely on the IDs. | |||
resetShadowRoot(vm); | |||
} | |||
|
|||
// Check that the template is built by the compiler | |||
secure.verifyTemplate(html); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
what do you think of renaming verifyTemplate to something like assertTemplateRegistered? When i see verify i think of a boolean return instead a throw. Just a food for thought to improve readability
|
||
import { | ||
TEMPLATE_FUNCTION_NAME, | ||
SECURE_IMPORT_NAME, SECURE_REGISTER_TEMPLATE_METHOD_NAME, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
pls move SECURE_REGISTER_TEMPLATE_METHOD_NAME to a new line
@@ -16,18 +23,47 @@ function moduleNameToImport(name: string): t.ImportDeclaration { | |||
); | |||
} | |||
|
|||
function generateSecureImport(): t.ImportDeclaration { | |||
return t.importDeclaration( | |||
[t.importSpecifier(t.identifier(SECURE_IMPORT_NAME), t.identifier(SECURE_IMPORT_NAME))], |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
are both importSpecifier parameters intentionally the same?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I will like to restrict and simplify this a little bit more.
- compiler should use
import { template } from "lwc/secure";
- validate does not need to be exposed (the engine can validate itself, locker has no saying on this).
enabled
is not needed, it should always be enabled.
The reason for 1, is that you don't have to restrict import *
, or the typescript definition, or any other future option for modules. It should come from its own, restricted module specifier that can only be use by the transformation, and can't be, ever, use by userland code, not even high privilege code that import those things.
In the future, I want to move api.ts
to be exposed via import { api } from "lwc/secure"
, so the same rules applies.
Reason for 2 is that we want to protect ourself, even on the open source, to prevent people from using manual creation of vnodes, since any future change my break them (look at VUE 3 as an example of this problem).
Reason for 3 is pretty much the same reason for 2.
@caridy You are missing some context:
Idealistically I would be ok making this non-optional, but the secure object intent was to be able to allow customization for all of this other security features that must be able to be configurable at runtime by the application owner. |
import { Template } from "../framework/template"; | ||
|
||
const DEFAULT_SECURE_ENABLEMENT = false; | ||
const VERIFIED_TEMPLATES = new Set(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
call this verifiedTemplateSet
verifyTemplate | ||
}; | ||
|
||
function registerTemplate(tmpl: Template) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this logic should probably belong into template.ts
@@ -97,6 +98,10 @@ export function evaluateTemplate(vm: VM, html: Template): Array<VNode|null> { | |||
// template, because they could have similar IDs, and snabbdom just rely on the IDs. | |||
resetShadowRoot(vm); | |||
} | |||
|
|||
// Check that the template is built by the compiler | |||
secure.verifyTemplate(html); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this should probably be a local function that does the validation against a local set.
Yes, we need to restrict access on a per module basis, not on a per export basis. cc @pmdartus |
Benchmark resultsBase commit: lwc-engine-benchmark
|
Benchmark resultsBase commit: lwc-engine-benchmark
|
Benchmark resultsBase commit: lwc-engine-benchmark
|
Benchmark resultsBase commit: lwc-engine-benchmark
|
Benchmark resultsBase commit: lwc-engine-benchmark
|
|
||
export function verifyTemplate(tmpl: Template): Template { | ||
if (!VERIFIED_TEMPLATE_SET.has(tmpl)) { | ||
throw new TypeError('Unknown template'); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this should probably throw a ReferenceError
instead. It should also provide more details about what component is at fault, otherwise you don't know much, this is because the render() method is called, and the result is the analyzed, so, throwing here will not have that render method in the error stack, which will make it very hard to know what's going on.
Probably in dev-mode we should have some nice logError with the proper component stack.
|
||
const VERIFIED_TEMPLATE_SET = new Set(); | ||
|
||
export function verifyTemplate(tmpl: Template): Template { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
the signature of this method is weird, it is a verification, but it returns the same template. Maybe calling this: isRegisteredTemplate(tmpl: Template): boolean
@@ -8,7 +8,8 @@ jest.mock('globalLib', () => { | |||
}); | |||
|
|||
describe('example-foo-inner', () => { | |||
it('default snapshot', () => { | |||
it.only('default snapshot', () => { | |||
debugger; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
remove.
@@ -8,7 +8,8 @@ jest.mock('globalLib', () => { | |||
}); | |||
|
|||
describe('example-foo-inner', () => { | |||
it('default snapshot', () => { | |||
it.only('default snapshot', () => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
remove .only
@@ -0,0 +1,16 @@ | |||
import { Template } from "./template"; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
why does this need to be separate from template.ts? it looks to me that it belongs there, so we don't have to export the verification api, it is all internal to that template.ts
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
few minor things... functionality wise, I think we are good. The biggest issue right now is the error usefulness.
Benchmark resultsBase commit: lwc-engine-benchmark
|
Details
Adding a mechanism to prevent templates to be created in user land.
The compiler will produce this new code:
The engine will have the references saved so it can know when anything is not coming from the compiler. Note that the named import
registerTemplate
must not be available for consumption in user-land.Does this PR introduce a breaking change?