Skip to content

feat: introduce @microsoft/webui-framework#146

Merged
mohamedmansour merged 7 commits intomainfrom
feat/webui-framework
Mar 27, 2026
Merged

feat: introduce @microsoft/webui-framework#146
mohamedmansour merged 7 commits intomainfrom
feat/webui-framework

Conversation

@mohamedmansour
Copy link
Copy Markdown
Contributor

@mohamedmansour mohamedmansour commented Mar 27, 2026

Introduce @microsoft/webui-framework, a lightweight Web Component runtime
that hydrates server-rendered HTML using compiled template metadata and
direct DOM binding. No virtual DOM, no runtime template parsing, no
JavaScript runtime required on the server.

 Introduce @microsoft/webui-framework — a lightweight Web Component runtime
 that hydrates server-rendered HTML using compiled template metadata and
 direct DOM binding. No virtual DOM, no runtime template parsing, no
 JavaScript runtime required on the server.

 - WebUIElement base class with @observable, @attr, @volatile decorators
 - Single-pass SSR hydration that seeds component state inline from markers
 - Per-path targeted updates via a binding index (O(affected) not O(all))
 - Template cloning cache for fast repeat instantiation (cloneNode not innerHTML)
 - Zero-allocation update path with pre-merged wildcard bindings
 - Event delegation (one listener per event type, not one closure per element)
 - Keyed and sequential repeat reconciliation for @for loops
 - Iterative condition AST evaluation for @if blocks (no recursion)
 - CSS module stylesheet cache via adoptedStyleSheets
 - Modular architecture: element.ts orchestrator + repeat, paths, conditions,
   seed, types, and styles modules with file-level documentation
 - Co-located unit tests (conditions, seed, types, template) and Playwright
   e2e fixtures covering hydration, reactivity, repeats, conditionals, events,
   refs, state seeding, CSS strategies, and performance benchmarks
 - Shared test infrastructure via @microsoft/webui-test-support (private)
 - HTML entity decoding in compiled text run static parts
 - Comprehensive README with architecture diagrams, performance philosophy,
   API reference, and contributor guidelines
 - Hydration docs, copilot instructions, and webui-dev skill updated
Comment thread packages/webui-framework/tests/fixtures/bench/element.ts Fixed
Comment thread packages/webui-framework/src/element.ts Fixed
cursor = cursor[key] as Record<string, unknown>;
}

cursor[parts[parts.length - 1]] = value;

Check warning

Code scanning / CodeQL

Prototype-polluting function Medium

The property chain
here
is recursively assigned to
cursor
without guarding against prototype pollution.

Copilot Autofix

AI 21 days ago

In general, the fix is to prevent recursive assignment along property chains when any segment is a dangerous key such as __proto__, constructor, or prototype. This can be done by either: (1) only recursing when the destination already has that own property and is a plain object, or (2) explicitly rejecting/bypassing assignments to those names. Because this function’s purpose is to construct nested objects along an arbitrary path, enforcing “must be an own property already on the destination” would change semantics. The least invasive and most compatible fix is to block dangerous keys.

Concretely, in assignSeedPath in packages/webui-framework/src/element/repeat.ts, we should introduce a small helper or inline check that treats any path containing a forbidden segment as a no-op (or early return). That means: when iterating through parts to build the path, if any key is __proto__, constructor, or prototype, we immediately stop and return without writing to cursor. We should also apply that same check to the final property name (parts[parts.length - 1]) before performing cursor[...]=value. This guarantees that no matter what untrusted path string is used, we never assign to prototype-polluting properties, while preserving all existing behavior for safe keys. No new imports are needed; the logic is local to this function.

Suggested changeset 1
packages/webui-framework/src/element/repeat.ts

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/packages/webui-framework/src/element/repeat.ts b/packages/webui-framework/src/element/repeat.ts
--- a/packages/webui-framework/src/element/repeat.ts
+++ b/packages/webui-framework/src/element/repeat.ts
@@ -176,9 +176,17 @@
 
 function assignSeedPath(target: Record<string, unknown>, path: string, value: unknown): void {
   const parts = path.split('.');
+
+  // Prevent prototype pollution via special property names in the path.
+  const forbiddenKeys = new Set(['__proto__', 'constructor', 'prototype']);
+
   let cursor = target;
   for (let index = 0; index < parts.length - 1; index += 1) {
     const key = parts[index];
+    if (forbiddenKeys.has(key)) {
+      // Abort if the path would write to a dangerous property.
+      return;
+    }
     const next = cursor[key];
     if (next == null || Array.isArray(next) || typeof next !== 'object') {
       cursor[key] = {};
@@ -186,7 +191,12 @@
     cursor = cursor[key] as Record<string, unknown>;
   }
 
-  cursor[parts[parts.length - 1]] = value;
+  const lastKey = parts[parts.length - 1];
+  if (forbiddenKeys.has(lastKey)) {
+    return;
+  }
+
+  cursor[lastKey] = value;
 }
 
 function parentNode(node: Node): (ParentNode & Node) | null {
EOF
@@ -176,9 +176,17 @@

function assignSeedPath(target: Record<string, unknown>, path: string, value: unknown): void {
const parts = path.split('.');

// Prevent prototype pollution via special property names in the path.
const forbiddenKeys = new Set(['__proto__', 'constructor', 'prototype']);

let cursor = target;
for (let index = 0; index < parts.length - 1; index += 1) {
const key = parts[index];
if (forbiddenKeys.has(key)) {
// Abort if the path would write to a dangerous property.
return;
}
const next = cursor[key];
if (next == null || Array.isArray(next) || typeof next !== 'object') {
cursor[key] = {};
@@ -186,7 +191,12 @@
cursor = cursor[key] as Record<string, unknown>;
}

cursor[parts[parts.length - 1]] = value;
const lastKey = parts[parts.length - 1];
if (forbiddenKeys.has(lastKey)) {
return;
}

cursor[lastKey] = value;
}

function parentNode(node: Node): (ParentNode & Node) | null {
Copilot is powered by AI and may make mistakes. Always verify output.
}

export function renderTemplateScript(name: string, meta: TemplateMeta): string {
return `<script>(function(){var w=window.__webui_templates||(window.__webui_templates={});w[${JSON.stringify(name)}]=${JSON.stringify(meta)};})();</script>`;

Check warning

Code scanning / CodeQL

Improper code sanitization Medium

Code construction depends on an
improperly sanitized value
.

Copilot Autofix

AI 21 days ago

In general, when generating HTML that contains inline JavaScript, any dynamic data interpolated into that JavaScript must be encoded in a way that is safe both for the JavaScript string literal and for the surrounding HTML <script> context. JSON.stringify handles JavaScript string escaping but does not guard against </script> and some other characters that can terminate the script tag when interpreted by an HTML parser. The fix is to post-process the JSON.stringify output (and, for consistency, the meta JSON too) to escape “unsafe” characters (<, >, /, backslash, control characters, and \u2028, \u2029) as \uXXXX sequences, following the pattern in the background example.

Concretely, in packages/webui-test-support/src/register-template.ts, we should:

  1. Add a small charMap and escapeUnsafeChars helper near renderTemplateScript. It takes a string and replaces any of the problematic characters with their safe escape sequences.
  2. Update renderTemplateScript to wrap the results of JSON.stringify(name) and JSON.stringify(meta) with escapeUnsafeChars(...) before interpolation into the template literal.
  3. Keep the function signature and overall behavior the same (still returns a <script>...</script> string, still uses JSON.stringify), only making the output safer.

No new external libraries are needed; we can implement this with plain TypeScript/JavaScript utilities in the same file.

Suggested changeset 1
packages/webui-test-support/src/register-template.ts

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/packages/webui-test-support/src/register-template.ts b/packages/webui-test-support/src/register-template.ts
--- a/packages/webui-test-support/src/register-template.ts
+++ b/packages/webui-test-support/src/register-template.ts
@@ -357,6 +357,27 @@
   templates[name] = normalizeTemplateMeta(meta);
 }
 
+const __unsafeCharMap: Record<string, string> = {
+  '<': '\\u003C',
+  '>': '\\u003E',
+  '/': '\\u002F',
+  '\\': '\\\\',
+  '\b': '\\b',
+  '\f': '\\f',
+  '\n': '\\n',
+  '\r': '\\r',
+  '\t': '\\t',
+  '\0': '\\0',
+  '\u2028': '\\u2028',
+  '\u2029': '\\u2029',
+};
+
+function escapeUnsafeChars(str: string): string {
+  return str.replace(/[<>/\\\b\f\n\r\t\0\u2028\u2029]/g, (ch) => __unsafeCharMap[ch] ?? ch);
+}
+
 export function renderTemplateScript(name: string, meta: TemplateMeta): string {
-  return `<script>(function(){var w=window.__webui_templates||(window.__webui_templates={});w[${JSON.stringify(name)}]=${JSON.stringify(meta)};})();</script>`;
+  const safeName = escapeUnsafeChars(JSON.stringify(name));
+  const safeMeta = escapeUnsafeChars(JSON.stringify(meta));
+  return `<script>(function(){var w=window.__webui_templates||(window.__webui_templates={});w[${safeName}]=${safeMeta};})();</script>`;
 }
EOF
@@ -357,6 +357,27 @@
templates[name] = normalizeTemplateMeta(meta);
}

const __unsafeCharMap: Record<string, string> = {
'<': '\\u003C',
'>': '\\u003E',
'/': '\\u002F',
'\\': '\\\\',
'\b': '\\b',
'\f': '\\f',
'\n': '\\n',
'\r': '\\r',
'\t': '\\t',
'\0': '\\0',
'\u2028': '\\u2028',
'\u2029': '\\u2029',
};

function escapeUnsafeChars(str: string): string {
return str.replace(/[<>/\\\b\f\n\r\t\0\u2028\u2029]/g, (ch) => __unsafeCharMap[ch] ?? ch);
}

export function renderTemplateScript(name: string, meta: TemplateMeta): string {
return `<script>(function(){var w=window.__webui_templates||(window.__webui_templates={});w[${JSON.stringify(name)}]=${JSON.stringify(meta)};})();</script>`;
const safeName = escapeUnsafeChars(JSON.stringify(name));
const safeMeta = escapeUnsafeChars(JSON.stringify(meta));
return `<script>(function(){var w=window.__webui_templates||(window.__webui_templates={});w[${safeName}]=${safeMeta};})();</script>`;
}
Copilot is powered by AI and may make mistakes. Always verify output.
Comment thread packages/webui-test-support/src/register-template.ts Dismissed
| { kind: 'repeat'; slot: ResolvedSlot; markerId: number; entry: [string, string, number] }
> = [];

for (const [index, [slotPath, parts]] of (meta.tx ?? []).entries()) {
mohamedmansour and others added 3 commits March 27, 2026 08:18
…tion or class'

Co-authored-by: Copilot Autofix powered by AI <223894421+github-code-quality[bot]@users.noreply.github.com>
mohamedmansour and others added 2 commits March 27, 2026 08:29
Replace snapshot .count() with expect.poll() so the assertion
retries until the product grid re-renders after client navigation.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@mohamedmansour mohamedmansour merged commit dd61d63 into main Mar 27, 2026
21 checks passed
@mohamedmansour mohamedmansour deleted the feat/webui-framework branch March 27, 2026 17:28
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.

3 participants