From 5e1b3d8f291913dfb9e8e75adbd6c26428fe00a0 Mon Sep 17 00:00:00 2001 From: osama-rizk Date: Thu, 9 Jul 2026 11:48:24 +0200 Subject: [PATCH 1/3] docs: add Gen 1 maintenance mode / EOL notice to README Amplify Gen 1 (including Amplify Studio and this UI component generator) is in maintenance mode. As of May 1, 2026 it receives only critical bug fixes and security patches; end of life is May 1, 2027. Mirrors the official announcement (aws-amplify/amplify-cli#14881) and links the Gen 1 to Gen 2 migration guide. --- README.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/README.md b/README.md index ec6469fad..ec5146e0c 100644 --- a/README.md +++ b/README.md @@ -13,6 +13,13 @@ [![Open Bugs](https://img.shields.io/github/issues/aws-amplify/amplify-codegen-ui/bug?color=d73a4a&label=bugs)](https://github.com/aws-amplify/amplify-codegen-ui/issues?q=is%3Aissue+is%3Aopen+label%3Abug) [![Feature Requests](https://img.shields.io/github/issues/aws-amplify/amplify-codegen-ui/feature-request?color=ff9001&label=feature%20requests)](https://github.com/aws-amplify/amplify-codegen-ui/issues?q=is%3Aissue+label%3Afeature-request+is%3Aopen) +> [!IMPORTANT] +> **Maintenance mode.** Amplify Gen 1 — including Amplify Studio and this UI +> component generator — is in maintenance mode. As of **May 1, 2026** it receives +> only critical bug fixes and security patches; **end of life is May 1, 2027**. +> See the [official announcement](https://github.com/aws-amplify/amplify-cli/issues/14881) +> and the [Gen 1 → Gen 2 migration guide](https://docs.amplify.aws/react/start/migrate-to-gen2/). + Generate React components for use in an AWS Amplify project. ## Usage From c29865835f79992d9d709648170e97093d630617 Mon Sep 17 00:00:00 2001 From: osama-rizk Date: Thu, 9 Jul 2026 12:22:39 +0200 Subject: [PATCH 2/3] fix(codegen-ui-react): emit non-identifier theme keys as string literals Theme keys were passed directly to factory.createIdentifier(), which emits its argument verbatim as source. Keys that are not valid JS identifiers could therefore produce malformed generated output. Route theme keys through a helper that emits valid identifiers as identifiers and any other key as a properly escaped string-literal key, keeping the generated object literal well-formed. No change for valid themes (existing snapshots unchanged). --- ...act-theme-studio-template-renderer.test.ts | 19 +++++++++++++++++++ .../react-theme-studio-template-renderer.ts | 17 +++++++++++++---- 2 files changed, 32 insertions(+), 4 deletions(-) diff --git a/packages/codegen-ui-react/lib/__tests__/react-theme-studio-template-renderer.test.ts b/packages/codegen-ui-react/lib/__tests__/react-theme-studio-template-renderer.test.ts index 3c179cab3..2446cab42 100644 --- a/packages/codegen-ui-react/lib/__tests__/react-theme-studio-template-renderer.test.ts +++ b/packages/codegen-ui-react/lib/__tests__/react-theme-studio-template-renderer.test.ts @@ -91,4 +91,23 @@ describe('react theme renderer tests', () => { expect(themeObject.breakpoints.defaultBreakpoint).toBe('base'); }); }); + + describe('theme keys that are not valid identifiers', () => { + it('should emit a non-identifier key as a quoted string-literal key', () => { + const nonIdentifierKey = 'foo("bar")'; + const theme: StudioTheme = { + name: 'MyTheme', + values: [{ key: nonIdentifierKey, value: { value: 'red' } }], + }; + const rendererFactory = new StudioTemplateRendererFactory( + (t: StudioTheme) => new ReactThemeStudioTemplateRenderer(t, {}), + ); + const { componentText } = rendererFactory.buildRenderer(theme).renderComponent(); + + // The key is emitted as a quoted, escaped string-literal key, keeping the + // generated object literal well-formed rather than emitting it verbatim. + expect(componentText).toContain('\'foo("bar")\': "red"'); + expect(componentText).not.toContain('foo("bar"): '); + }); + }); }); diff --git a/packages/codegen-ui-react/lib/react-theme-studio-template-renderer.ts b/packages/codegen-ui-react/lib/react-theme-studio-template-renderer.ts index 6f6d4adcc..3da07ea2d 100644 --- a/packages/codegen-ui-react/lib/react-theme-studio-template-renderer.ts +++ b/packages/codegen-ui-react/lib/react-theme-studio-template-renderer.ts @@ -18,6 +18,7 @@ import { factory, EmitHint, ExportAssignment, + Identifier, ObjectLiteralExpression, StringLiteral, PropertyAssignment, @@ -47,6 +48,14 @@ export type ReactThemeStudioTemplateRendererOptions = { renderDefaultTheme?: boolean; }; +// Theme keys can be arbitrary strings, but factory.createIdentifier() emits its +// argument verbatim as source. Emit only valid JS identifiers as identifiers; +// any other key is emitted as a (properly escaped) string-literal key instead, +// which keeps the generated object literal well-formed. +function buildThemePropertyName(key: string): Identifier | StringLiteral { + return /^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(key) ? factory.createIdentifier(key) : factory.createStringLiteral(key); +} + export class ReactThemeStudioTemplateRenderer extends StudioTemplateRenderer< string, StudioTheme, @@ -162,9 +171,9 @@ export class ReactThemeStudioTemplateRenderer extends StudioTemplateRenderer< splitAndBuildThemeValues(values: StudioThemeValues[]): PropertyAssignment[] { return values.map(({ key, value }) => { if (key !== 'breakpoints') { - return factory.createPropertyAssignment(factory.createIdentifier(key), this.buildThemeValue(value)); + return factory.createPropertyAssignment(buildThemePropertyName(key), this.buildThemeValue(value)); } - return factory.createPropertyAssignment(factory.createIdentifier(key), this.buildThemeBreakpointValue(value)); + return factory.createPropertyAssignment(buildThemePropertyName(key), this.buildThemeBreakpointValue(value)); }); } @@ -178,7 +187,7 @@ export class ReactThemeStudioTemplateRenderer extends StudioTemplateRenderer< */ private buildThemeValues(values: StudioThemeValues[]): PropertyAssignment[] { return values.map(({ key, value }) => - factory.createPropertyAssignment(factory.createIdentifier(key), this.buildThemeValue(value)), + factory.createPropertyAssignment(buildThemePropertyName(key), this.buildThemeValue(value)), ); } @@ -207,7 +216,7 @@ export class ReactThemeStudioTemplateRenderer extends StudioTemplateRenderer< */ buildThemeBreakpointValues(values: StudioThemeValues[]): PropertyAssignment[] { return values.map(({ key, value }) => - factory.createPropertyAssignment(factory.createIdentifier(key), this.buildThemeBreakpointValue(value)), + factory.createPropertyAssignment(buildThemePropertyName(key), this.buildThemeBreakpointValue(value)), ); } From 6d9614cb6402fadc29b4248feae3e7af5e8b3d14 Mon Sep 17 00:00:00 2001 From: osama-rizk Date: Thu, 9 Jul 2026 15:14:15 +0200 Subject: [PATCH 3/3] refactor(codegen-ui-react): share identifier regex and harden theme-key tests - Extract SIMPLE_JS_IDENTIFIER_RE to lib/utils/identifiers.ts; use it in the theme renderer and events.ts (removes duplicate literals). - Export buildThemePropertyName and add a direct unit block covering the identifier/non-identifier boundary. - Add explicit attack-payload tests (object-literal breakout, indirect eval, string-concat) and per-sink tests for buildThemeValues and buildThemeBreakpointValues. - Split test payload into a legitimate CSS-key case and labelled injection cases; add SECURITY rationale comment and document the string-literal fallback. --- ...act-theme-studio-template-renderer.test.ts | 96 ++++++++++++++++--- .../react-theme-studio-template-renderer.ts | 22 +++-- .../codegen-ui-react/lib/utils/identifiers.ts | 25 +++++ .../codegen-ui-react/lib/workflow/events.ts | 3 +- 4 files changed, 125 insertions(+), 21 deletions(-) create mode 100644 packages/codegen-ui-react/lib/utils/identifiers.ts diff --git a/packages/codegen-ui-react/lib/__tests__/react-theme-studio-template-renderer.test.ts b/packages/codegen-ui-react/lib/__tests__/react-theme-studio-template-renderer.test.ts index 2446cab42..d047520c5 100644 --- a/packages/codegen-ui-react/lib/__tests__/react-theme-studio-template-renderer.test.ts +++ b/packages/codegen-ui-react/lib/__tests__/react-theme-studio-template-renderer.test.ts @@ -13,11 +13,13 @@ See the License for the specific language governing permissions and limitations under the License. */ +import { isIdentifier, isStringLiteral } from 'typescript'; import { StudioTemplateRendererFactory, StudioTheme } from '@aws-amplify/codegen-ui'; import { ScriptTarget, ScriptKind, ReactRenderConfig } from '..'; import { ReactThemeStudioTemplateRenderer, ReactThemeStudioTemplateRendererOptions, + buildThemePropertyName, } from '../react-theme-studio-template-renderer'; import { loadSchemaFromJSONFile } from './__utils__'; @@ -92,22 +94,88 @@ describe('react theme renderer tests', () => { }); }); + // SECURITY: theme keys are attacker-controllable and reach factory.createIdentifier() + // (CVE-2025-4318 class). buildThemePropertyName must emit only valid identifiers as + // identifiers and everything else as an inert quoted string-literal key. + describe('buildThemePropertyName', () => { + it.each([ + ['simple identifier', 'colors'], + ['underscore prefix', '_private'], + ['dollar prefix', '$ref'], + ['digits after letter', 'space2'], + ])('emits a valid identifier (%s) as an Identifier node', (_label, key) => { + const node = buildThemePropertyName(key); + expect(isIdentifier(node)).toBe(true); + expect(isStringLiteral(node)).toBe(false); + }); + + it.each([ + ['empty string', ''], + ['leading digit', '2xl'], + ['css custom property', '--spacing-1'], + ['hyphenated css key', 'font-size'], + ['object-literal breakout', '}};process.exit(1);//'], + ['indirect eval', '(0,eval)("x")'], + ['string-concat obfuscation', '"a"+"b"'], + ['null-byte smuggling', 'foo\x00bar'], + ])('emits a non-identifier key (%s) as a StringLiteral node', (_label, key) => { + const node = buildThemePropertyName(key); + expect(isStringLiteral(node)).toBe(true); + expect(isIdentifier(node)).toBe(false); + }); + }); + describe('theme keys that are not valid identifiers', () => { - it('should emit a non-identifier key as a quoted string-literal key', () => { - const nonIdentifierKey = 'foo("bar")'; - const theme: StudioTheme = { - name: 'MyTheme', - values: [{ key: nonIdentifierKey, value: { value: 'red' } }], - }; - const rendererFactory = new StudioTemplateRendererFactory( - (t: StudioTheme) => new ReactThemeStudioTemplateRenderer(t, {}), - ); - const { componentText } = rendererFactory.buildRenderer(theme).renderComponent(); + const rendererFactory = new StudioTemplateRendererFactory( + (t: StudioTheme) => new ReactThemeStudioTemplateRenderer(t, {}), + ); + + const renderWithKey = (key: string, values: StudioTheme['values']): string => + rendererFactory.buildRenderer({ name: 'MyTheme', values }).renderComponent().componentText; + + it('emits a legitimate non-identifier CSS key as a quoted string-literal key', () => { + // Documents the legitimate path: CSS custom-property-style keys are valid data, + // just not valid JS identifiers, so they are emitted as quoted keys. + const componentText = renderWithKey('font-size', [{ key: 'font-size', value: { value: 'red' } }]); + expect(componentText).toMatch(/['"]font-size['"]\s*:/); + }); + + const escapeRe = (s: string): string => s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); + + // Assert `key` is emitted only as a quoted property name (either quote style), + // and never as a bare (unquoted) property key that would execute as source. + const expectNeutralized = (componentText: string, key: string): void => { + const escaped = escapeRe(key); + expect(componentText).toMatch(new RegExp(`['"]${escaped}['"]\\s*:`)); + expect(componentText).not.toMatch(new RegExp(`(? { + expectNeutralized(renderWithKey(key, [{ key, value: { value: 'red' } }]), key); + }); + + // Per-sink coverage: buildThemeValues (nested token keys) and buildThemeBreakpointValues + // (breakpoint token keys) both delegate to buildThemePropertyName. Exercise each so a + // future refactor that inlines the call differently cannot silently regress. + it('neutralizes attack key at the nested buildThemeValues sink', () => { + const attackKey = '(0,eval)("x")'; + const componentText = renderWithKey('tokens', [ + { key: 'tokens', value: { children: [{ key: attackKey, value: { value: 'red' } }] } }, + ]); + expectNeutralized(componentText, attackKey); + }); - // The key is emitted as a quoted, escaped string-literal key, keeping the - // generated object literal well-formed rather than emitting it verbatim. - expect(componentText).toContain('\'foo("bar")\': "red"'); - expect(componentText).not.toContain('foo("bar"): '); + it('neutralizes attack key at the buildThemeBreakpointValues sink', () => { + const attackKey = '(0,eval)("x")'; + const componentText = renderWithKey('breakpoints', [ + { key: 'breakpoints', value: { children: [{ key: attackKey, value: { value: '480' } }] } }, + ]); + expectNeutralized(componentText, attackKey); }); }); }); diff --git a/packages/codegen-ui-react/lib/react-theme-studio-template-renderer.ts b/packages/codegen-ui-react/lib/react-theme-studio-template-renderer.ts index 3da07ea2d..a60b8bb35 100644 --- a/packages/codegen-ui-react/lib/react-theme-studio-template-renderer.ts +++ b/packages/codegen-ui-react/lib/react-theme-studio-template-renderer.ts @@ -33,6 +33,7 @@ import { InvalidInputError, handleCodegenErrors, } from '@aws-amplify/codegen-ui'; +import { SIMPLE_JS_IDENTIFIER_RE } from './utils/identifiers'; import { ReactRenderConfig, scriptKindToFileExtensionNonReact } from './react-render-config'; import { ImportCollection, ImportValue } from './imports'; import { ReactOutputManager } from './react-output-manager'; @@ -48,12 +49,21 @@ export type ReactThemeStudioTemplateRendererOptions = { renderDefaultTheme?: boolean; }; -// Theme keys can be arbitrary strings, but factory.createIdentifier() emits its -// argument verbatim as source. Emit only valid JS identifiers as identifiers; -// any other key is emitted as a (properly escaped) string-literal key instead, -// which keeps the generated object literal well-formed. -function buildThemePropertyName(key: string): Identifier | StringLiteral { - return /^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(key) ? factory.createIdentifier(key) : factory.createStringLiteral(key); +// SECURITY: passing attacker-controlled StudioTheme keys directly to +// factory.createIdentifier() allows code injection in generated .tsx output +// (CVE-2025-4318 class -- theme-renderer surface). createIdentifier() emits its +// argument verbatim as source, so only valid JS identifiers are emitted as +// identifiers; any other key is emitted as a (properly escaped) string-literal +// key instead. +// +// Note: unlike escapePropertyValue()/buildBindingEvent() in the component path +// -- which fall back to '' (an empty identifier) -- we intentionally fall back +// to the full key as a string literal. This keeps the generated object literal +// well-formed (valid syntax, no empty identifier) and preserves legitimate +// non-identifier CSS token keys such as '--spacing-1' and '2xl'. Do not +// normalize this to the '' fallback. +export function buildThemePropertyName(key: string): Identifier | StringLiteral { + return SIMPLE_JS_IDENTIFIER_RE.test(key) ? factory.createIdentifier(key) : factory.createStringLiteral(key); } export class ReactThemeStudioTemplateRenderer extends StudioTemplateRenderer< diff --git a/packages/codegen-ui-react/lib/utils/identifiers.ts b/packages/codegen-ui-react/lib/utils/identifiers.ts new file mode 100644 index 000000000..9e182a510 --- /dev/null +++ b/packages/codegen-ui-react/lib/utils/identifiers.ts @@ -0,0 +1,25 @@ +/* + Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + + Licensed under the Apache License, Version 2.0 (the "License"). + You may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + */ + +/** + * Matches a simple JS identifier -- no dot-paths, no computed access. + * + * SECURITY: values that fail this test must not be passed to + * factory.createIdentifier(), which emits its argument verbatim as source and + * would otherwise allow code injection into generated output + * (CVE-2025-4318 class). + */ +export const SIMPLE_JS_IDENTIFIER_RE = /^[a-zA-Z_$][a-zA-Z0-9_$]*$/; diff --git a/packages/codegen-ui-react/lib/workflow/events.ts b/packages/codegen-ui-react/lib/workflow/events.ts index e834c0474..0769ba93d 100644 --- a/packages/codegen-ui-react/lib/workflow/events.ts +++ b/packages/codegen-ui-react/lib/workflow/events.ts @@ -19,6 +19,7 @@ import { getActionIdentifier } from './action'; import { isBoundEvent, isActionEvent } from '../react-component-render-helper'; import keywords from '../keywords'; import { Primitive, PrimitiveLevelPropConfiguration } from '../primitive'; +import { SIMPLE_JS_IDENTIFIER_RE } from '../utils/identifiers'; /* * Temporary hardcoded mapping of generic to react events, long-term this will be exported by amplify-ui. @@ -77,7 +78,7 @@ export function buildBindingEvent( if (keywords.has(bindingEvent)) { bindingEvent = `${bindingEvent}Prop`; } - const isValidIdentifier = /^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(bindingEvent); + const isValidIdentifier = SIMPLE_JS_IDENTIFIER_RE.test(bindingEvent); const sanitizedBindingEvent = isValidIdentifier ? bindingEvent : ''; const expr = factory.createIdentifier(sanitizedBindingEvent); return factory.createJsxAttribute(