Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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__';

Expand Down Expand Up @@ -91,4 +93,89 @@ describe('react theme renderer tests', () => {
expect(themeObject.breakpoints.defaultBreakpoint).toBe('base');
});
});

// 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', () => {
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(`(?<!['"])${escaped}(?!['"])\\s*:`));
};

// SECURITY: each payload is a known CVE-2025-4318-class bypass form.
it.each([
['object-literal breakout', '}};process.exit(1);//'],
['indirect eval', '(0,eval)("x")'],
['string-concat obfuscation', '"a"+"b"'],
])('neutralizes attack key at the values sink: %s', (_label, key) => {
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);
});

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);
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import {
factory,
EmitHint,
ExportAssignment,
Identifier,
ObjectLiteralExpression,
StringLiteral,
PropertyAssignment,
Expand All @@ -32,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';
Expand All @@ -47,6 +49,23 @@ export type ReactThemeStudioTemplateRendererOptions = {
renderDefaultTheme?: boolean;
};

// 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);
}
Comment thread
soberm marked this conversation as resolved.

export class ReactThemeStudioTemplateRenderer extends StudioTemplateRenderer<
string,
StudioTheme,
Expand Down Expand Up @@ -162,9 +181,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));
});
}

Expand All @@ -178,7 +197,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)),
);
}

Expand Down Expand Up @@ -207,7 +226,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)),
);
}

Expand Down
25 changes: 25 additions & 0 deletions packages/codegen-ui-react/lib/utils/identifiers.ts
Original file line number Diff line number Diff line change
@@ -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_$]*$/;
3 changes: 2 additions & 1 deletion packages/codegen-ui-react/lib/workflow/events.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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(
Expand Down
Loading