Skip to content

Home > Documentation > Security > Message Validation

Message Validation ​

Complete guide to validating message payloads in ParleyJS for secure cross-window communication.

Table of Contents ​

  1. Overview
  2. Why Validate Messages
  3. JSON Schema Validation
  4. Type Validation
  5. Content Sanitization
  6. Validation Libraries
  7. Error Handling
  8. Security Best Practices
  9. Complete Examples

Overview ​

Message validation ensures that incoming payloads match expected structures and types before processing. ParleyJS provides built-in validation support through JSON Schema and custom validators.

Validation prevents:

  • Type errors from malformed data
  • Security vulnerabilities from unexpected content
  • Application crashes from invalid payloads
  • Data corruption from incorrect types

Why Validate Messages ​

Cross-window communication involves untrusted data sources. Always validate messages to protect your application.

Security Risks Without Validation ​

XSS via Messages:

typescript
// DANGEROUS - No validation
parley.on('display-message', (payload, respond) => {
    document.getElementById('output').innerHTML = payload.html; // XSS!
});

Type Confusion:

typescript
// DANGEROUS - Assumes number
parley.on('set-quantity', (payload, respond) => {
    const total = payload.quantity * price; // What if quantity is a string?
});

Prototype Pollution:

typescript
// DANGEROUS - No validation
parley.on('update-settings', (payload, respond) => {
    Object.assign(settings, payload); // Could pollute Object.prototype!
});

Benefits of Validation ​

  • Catches errors early before data processing
  • Provides clear error messages for debugging
  • Documents expected message structure
  • Prevents security vulnerabilities
  • Enables type-safe communication

JSON Schema Validation ​

ParleyJS supports JSON Schema for declarative validation.

Basic Schema Validation ​

typescript
import { Parley } from 'parley-js';

const parley = Parley.create({
    allowedOrigins: [window.location.origin],
});

// Define JSON Schema for user data
const userSchema = {
    type: 'object',
    properties: {
        userId: { type: 'number' },
        username: { type: 'string', minLength: 3, maxLength: 20 },
        email: { type: 'string', format: 'email' },
        role: { type: 'string', enum: ['admin', 'user', 'guest'] },
    },
    required: ['userId', 'username', 'email'],
    additionalProperties: false,
};

// Register handler with schema validation
parley.on(
    'create-user',
    (payload, respond, metadata) => {
        // Schema validated automatically by ParleyJS
        const { userId, username, email, role } = payload;

        // Process validated data safely
        const user = createUser({ userId, username, email, role });
        respond({ success: true, user });
    },
    {
        schema: userSchema,
    }
);

Schema Validation with SchemaValidator ​

typescript
import { SchemaValidator } from 'parley-js';

const validator = new SchemaValidator();

// Define schema
const messageSchema = {
    type: 'object',
    properties: {
        action: { type: 'string' },
        payload: {
            type: 'object',
            properties: {
                id: { type: 'number' },
                data: { type: 'string' },
            },
            required: ['id'],
        },
    },
    required: ['action', 'payload'],
};

// Validate manually
parley.on('process-action', (payload, respond) => {
    const result = validator.validate(payload, messageSchema);

    if (!result.valid) {
        throw new ValidationError('Invalid payload', result.errors);
    }

    // Process validated payload
    handleAction(payload);
    respond({ success: true });
});

Common JSON Schema Patterns ​

Enum Values:

typescript
const schema = {
    type: 'object',
    properties: {
        status: {
            type: 'string',
            enum: ['pending', 'active', 'completed', 'cancelled'],
        },
    },
};

Array Validation:

typescript
const schema = {
    type: 'object',
    properties: {
        items: {
            type: 'array',
            items: {
                type: 'object',
                properties: {
                    id: { type: 'number' },
                    name: { type: 'string' },
                },
                required: ['id', 'name'],
            },
            minItems: 1,
            maxItems: 100,
        },
    },
};

Nested Objects:

typescript
const schema = {
    type: 'object',
    properties: {
        user: {
            type: 'object',
            properties: {
                profile: {
                    type: 'object',
                    properties: {
                        name: { type: 'string' },
                        age: { type: 'number', minimum: 0, maximum: 150 },
                    },
                },
            },
        },
    },
};

Type Validation ​

TypeScript provides compile-time type checking. Combine with runtime validation for complete safety.

TypeScript Interface Validation ​

typescript
interface UpdateRequest {
    userId: number;
    updates: {
        email?: string;
        name?: string;
    };
}

interface UpdateResponse {
    success: boolean;
    user?: {
        userId: number;
        email: string;
        name: string;
    };
    error?: string;
}

// TypeScript ensures type safety
parley.on<UpdateRequest>(
    'update-user',
    async (payload, respond: ResponseFunction<UpdateResponse>) => {
        // Runtime validation
        if (typeof payload.userId !== 'number') {
            respond({ success: false, error: 'Invalid userId type' });
            return;
        }

        if (payload.userId <= 0) {
            respond({ success: false, error: 'userId must be positive' });
            return;
        }

        // Process validated payload
        const user = await updateUser(payload.userId, payload.updates);
        respond({ success: true, user });
    }
);

Custom Type Guards ​

typescript
interface UserPayload {
    userId: number;
    username: string;
}

function isUserPayload(payload: unknown): payload is UserPayload {
    return (
        typeof payload === 'object' &&
        payload !== null &&
        'userId' in payload &&
        'username' in payload &&
        typeof (payload as UserPayload).userId === 'number' &&
        typeof (payload as UserPayload).username === 'string' &&
        (payload as UserPayload).username.length > 0
    );
}

parley.on('process-user', (payload, respond) => {
    if (!isUserPayload(payload)) {
        throw new ValidationError('Invalid user payload');
    }

    // TypeScript knows payload is UserPayload here
    const { userId, username } = payload;
    processUser(userId, username);
    respond({ success: true });
});

Content Sanitization ​

Sanitize HTML and dangerous content before processing or displaying messages.

HTML Sanitization ​

typescript
import DOMPurify from 'dompurify'; // External library

parley.on('display-content', (payload, respond) => {
    // NEVER trust HTML from messages
    const sanitized = DOMPurify.sanitize(payload.html, {
        ALLOWED_TAGS: ['b', 'i', 'em', 'strong', 'p'],
        ALLOWED_ATTR: [],
    });

    document.getElementById('content').innerHTML = sanitized;
    respond({ success: true });
});

String Sanitization ​

typescript
function sanitizeString(input: unknown): string {
    if (typeof input !== 'string') {
        throw new ValidationError('Input must be a string');
    }

    // Remove null bytes
    let sanitized = input.replace(/\0/g, '');

    // Limit length
    if (sanitized.length > 1000) {
        sanitized = sanitized.substring(0, 1000);
    }

    // Remove control characters except newline/tab
    sanitized = sanitized.replace(/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/g, '');

    return sanitized;
}

parley.on('submit-comment', (payload, respond) => {
    const comment = sanitizeString(payload.text);
    saveComment(comment);
    respond({ success: true });
});

URL Validation ​

typescript
function isValidUrl(url: unknown): url is string {
    if (typeof url !== 'string') return false;

    try {
        const parsed = new URL(url);
        // Only allow http/https protocols
        return parsed.protocol === 'http:' || parsed.protocol === 'https:';
    } catch {
        return false;
    }
}

parley.on('open-link', (payload, respond) => {
    if (!isValidUrl(payload.url)) {
        respond({ success: false, error: 'Invalid URL' });
        return;
    }

    window.open(payload.url, '_blank', 'noopener,noreferrer');
    respond({ success: true });
});

Validation Libraries ​

Integrate popular validation libraries with ParleyJS.

Zod ​

typescript
import { z } from 'zod';

const userSchema = z.object({
    userId: z.number().int().positive(),
    email: z.string().email(),
    age: z.number().int().min(0).max(150).optional(),
    role: z.enum(['admin', 'user', 'guest']),
});

type User = z.infer<typeof userSchema>;

parley.on<unknown>('create-user', (payload, respond) => {
    try {
        // Zod validates and transforms
        const user = userSchema.parse(payload);

        // user is now typed as User
        createUser(user);
        respond({ success: true, user });
    } catch (error) {
        if (error instanceof z.ZodError) {
            respond({
                success: false,
                errors: error.errors.map((e) => ({
                    path: e.path.join('.'),
                    message: e.message,
                })),
            });
        }
    }
});

Yup ​

typescript
import * as yup from 'yup';

const messageSchema = yup.object({
    type: yup.string().required().oneOf(['info', 'warning', 'error']),
    message: yup.string().required().min(1).max(500),
    timestamp: yup.number().required().positive(),
});

parley.on('log-message', async (payload, respond) => {
    try {
        const validated = await messageSchema.validate(payload, {
            abortEarly: false,
            stripUnknown: true,
        });

        logMessage(validated);
        respond({ success: true });
    } catch (error) {
        if (error instanceof yup.ValidationError) {
            respond({ success: false, errors: error.errors });
        }
    }
});

Joi ​

typescript
import Joi from 'joi';

const configSchema = Joi.object({
    apiKey: Joi.string().alphanum().length(32).required(),
    endpoint: Joi.string().uri().required(),
    timeout: Joi.number().integer().min(1000).max(30000).default(5000),
    retries: Joi.number().integer().min(0).max(5).default(3),
});

parley.on('update-config', (payload, respond) => {
    const { error, value } = configSchema.validate(payload, {
        abortEarly: false,
        stripUnknown: true,
    });

    if (error) {
        respond({
            success: false,
            errors: error.details.map((d) => d.message),
        });
        return;
    }

    updateConfig(value);
    respond({ success: true, config: value });
});

Error Handling ​

Handle validation errors gracefully and provide helpful feedback.

ValidationError Handling ​

typescript
import { ValidationError } from 'parley-js';

parley.on('process-data', (payload, respond) => {
    try {
        validatePayload(payload);
        processData(payload);
        respond({ success: true });
    } catch (error) {
        if (error instanceof ValidationError) {
            // Validation error with details
            respond({
                success: false,
                error: 'Validation failed',
                details: error.validationErrors,
            });
        } else {
            // Unexpected error
            respond({
                success: false,
                error: 'Processing failed',
            });
        }
    }
});

Detailed Error Reporting ​

typescript
function validateAndRespond(
    payload: unknown,
    schema: object,
    respond: ResponseFunction
) {
    const result = validator.validate(payload, schema);

    if (!result.valid) {
        const errors = result.errors.map((err) => ({
            field: err.field,
            message: err.message,
            expected: err.expected,
            received: err.received,
        }));

        respond({
            success: false,
            validationErrors: errors,
        });
        return false;
    }

    return true;
}

parley.on('submit-form', (payload, respond) => {
    if (!validateAndRespond(payload, formSchema, respond)) {
        return; // Validation failed, error already sent
    }

    // Process validated payload
    submitForm(payload);
    respond({ success: true });
});

Security Best Practices ​

Validate Everything ​

typescript
// ALWAYS validate before processing
parley.on('update-settings', (payload, respond) => {
    // 1. Type validation
    if (typeof payload !== 'object' || payload === null) {
        throw new ValidationError('Payload must be an object');
    }

    // 2. Schema validation
    const result = validator.validate(payload, settingsSchema);
    if (!result.valid) {
        throw new ValidationError('Schema validation failed', result.errors);
    }

    // 3. Business logic validation
    if (payload.maxUsers < payload.currentUsers) {
        respond({
            success: false,
            error: 'maxUsers cannot be less than currentUsers',
        });
        return;
    }

    // 4. Sanitization
    const sanitized = sanitizeSettings(payload);

    // 5. Process
    updateSettings(sanitized);
    respond({ success: true });
});

Whitelist, Not Blacklist ​

typescript
// GOOD - Whitelist allowed fields
function extractAllowedFields(payload: unknown): Settings {
    if (typeof payload !== 'object' || payload === null) {
        throw new ValidationError('Invalid payload');
    }

    const allowed: Settings = {
        theme: isValidTheme(payload.theme) ? payload.theme : 'light',
        language: isValidLanguage(payload.language) ? payload.language : 'en',
        notifications:
            typeof payload.notifications === 'boolean'
                ? payload.notifications
                : true,
    };

    return allowed;
}

// BAD - Trying to blacklist dangerous fields (incomplete)
function removeProtoFields(payload: object) {
    delete payload.__proto__;
    delete payload.constructor;
    return payload; // Still vulnerable!
}

Size Limits ​

typescript
const MAX_PAYLOAD_SIZE = 1024 * 100; // 100 KB

parley.on('upload-data', (payload, respond) => {
    const payloadSize = JSON.stringify(payload).length;

    if (payloadSize > MAX_PAYLOAD_SIZE) {
        respond({
            success: false,
            error: `Payload too large: ${payloadSize} bytes (max: ${MAX_PAYLOAD_SIZE})`,
        });
        return;
    }

    processData(payload);
    respond({ success: true });
});

Reject Unknown Properties ​

typescript
const strictSchema = {
    type: 'object',
    properties: {
        action: { type: 'string' },
        data: { type: 'object' },
    },
    required: ['action'],
    additionalProperties: false, // Reject unknown properties
};

Complete Examples ​

Secure User Registration ​

typescript
import { Parley, ValidationError } from 'parley-js';
import { z } from 'zod';

const registerSchema = z.object({
    username: z
        .string()
        .min(3)
        .max(20)
        .regex(/^[a-zA-Z0-9_]+$/),
    email: z.string().email(),
    password: z.string().min(8).max(100),
    agreeToTerms: z.boolean().refine((val) => val === true, {
        message: 'Must agree to terms',
    }),
});

const parley = Parley.create({
    allowedOrigins: ['https://app.example.com'],
});

parley.on('register-user', async (payload, respond) => {
    try {
        // Validate with Zod
        const validated = registerSchema.parse(payload);

        // Additional business logic validation
        const userExists = await checkUserExists(validated.username);
        if (userExists) {
            respond({
                success: false,
                error: 'Username already taken',
            });
            return;
        }

        // Hash password before storing
        const hashedPassword = await hashPassword(validated.password);

        // Create user
        const user = await createUser({
            username: validated.username,
            email: validated.email,
            password: hashedPassword,
        });

        respond({ success: true, userId: user.id });
    } catch (error) {
        if (error instanceof z.ZodError) {
            respond({
                success: false,
                validationErrors: error.errors.map((e) => ({
                    field: e.path.join('.'),
                    message: e.message,
                })),
            });
        } else {
            respond({
                success: false,
                error: 'Registration failed',
            });
        }
    }
});

Safe Data Processing ​

typescript
const dataSchema = {
    type: 'object',
    properties: {
        items: {
            type: 'array',
            items: {
                type: 'object',
                properties: {
                    id: { type: 'string', pattern: '^[a-zA-Z0-9-]+$' },
                    value: { type: 'number', minimum: 0 },
                    label: { type: 'string', maxLength: 100 },
                },
                required: ['id', 'value'],
                additionalProperties: false,
            },
            maxItems: 1000,
        },
    },
    required: ['items'],
    additionalProperties: false,
};

parley.on('process-batch', (payload, respond) => {
    // Schema validation
    const result = validator.validate(payload, dataSchema);
    if (!result.valid) {
        respond({
            success: false,
            errors: result.errors,
        });
        return;
    }

    // Additional validation
    const uniqueIds = new Set(payload.items.map((item) => item.id));
    if (uniqueIds.size !== payload.items.length) {
        respond({
            success: false,
            error: 'Duplicate IDs found',
        });
        return;
    }

    // Process validated data
    const results = payload.items.map((item) => processItem(item));
    respond({ success: true, results });
});


Previous: Origin Validation Next: Security Best Practices Related: Error Handling

Back to: Security | Documentation Home

Released under the MIT License.