/*
* 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.
*/
/**
* Input validation and output encoding tests
* Tests OWASP-compliant data processing and validation
*/
import { describe, it, expect } from '@jest/globals';
import { InputValidators, OutputEncoders, processing } from '../src/security/index';
describe('Data Processing Tests', () => {
const validators = new InputValidators();
const encoders = new OutputEncoders();
describe('Input Validation', () => {
describe('Query Validation', () => {
it('should accept valid queries', () => {
const result = validators.validateQuery('How to search products');
expect(result.isValid).toBe(true);
expect(result.sanitizedValue).toBe('How to search products');
});
it('should reject script tags', () => {
const result = validators.validateQuery('<script>alert("xss")</script>');
expect(result.isValid).toBe(false);
expect(result.errors).toContain('Query contains potentially malicious patterns');
});
it('should reject JavaScript protocols', () => {
const result = validators.validateQuery('javascript:alert("xss")');
expect(result.isValid).toBe(false);
expect(result.errors).toContain('Query contains potentially malicious patterns');
});
it('should reject event handlers', () => {
const result = validators.validateQuery('onclick=alert("xss")');
expect(result.isValid).toBe(false);
expect(result.errors).toContain('Query contains potentially malicious patterns');
});
it('should reject SQL injection attempts', () => {
const result = validators.validateQuery('SELECT * FROM users');
expect(result.isValid).toBe(false);
expect(result.errors).toContain('Query contains potentially malicious patterns');
});
it('should enforce length limits', () => {
const longQuery = 'a'.repeat(2000);
const result = validators.validateQuery(longQuery);
expect(result.isValid).toBe(false);
expect(result.errors).toContain('Query too long (maximum 1000 characters)');
});
it('should reject invalid characters', () => {
const result = validators.validateQuery('query\x00with\x01control\x02chars');
expect(result.isValid).toBe(false);
expect(result.errors).toContain('Query contains invalid characters');
});
});
describe('Document Reference Validation', () => {
it('should accept valid document references', () => {
const result = validators.validateDocumentReference('docs/api-guide.md');
expect(result.isValid).toBe(true);
expect(result.sanitizedValue).toBe('docs/api-guide.md');
});
it('should reject path traversal attempts', () => {
const testCases = [
'../../../etc/passwd',
'..\\..\\windows\\system32',
'%2e%2e%2f%2e%2e%2f',
'docs/../../secret.txt',
];
testCases.forEach(testCase => {
const result = validators.validateDocumentReference(testCase);
expect(result.isValid).toBe(false);
expect(result.errors).toContain('Document reference contains path traversal attempts');
});
});
it('should reject absolute paths', () => {
const testCases = [
'/etc/passwd',
'C:\\Windows\\System32',
'/home/user/secret',
];
testCases.forEach(testCase => {
const result = validators.validateDocumentReference(testCase);
expect(result.isValid).toBe(false);
expect(result.errors).toContain('Absolute paths are not allowed');
});
});
it('should validate file extensions', () => {
const result = validators.validateDocumentReference('malicious.exe');
expect(result.isValid).toBe(false);
expect(result.errors).toContain("File extension '.exe' is not allowed");
});
it('should accept allowed file extensions', () => {
const validFiles = [
'guide.md',
'config.json',
'readme.txt',
'schema.yml',
'data.yaml',
'content.xml',
];
validFiles.forEach(file => {
const result = validators.validateDocumentReference(file);
expect(result.isValid).toBe(true);
});
});
it('should accept document references with spaces', () => {
const result = validators.validateDocumentReference('Guides and FAQs/amazon-business-apis-getting-started-guide.md');
expect(result.isValid).toBe(true);
expect(result.sanitizedValue).toBe('Guides and FAQs/amazon-business-apis-getting-started-guide.md');
});
it('should accept document references with spaces in various formats', () => {
const validRefsWithSpaces = [
'Product Search API/product-search-model.md',
'OAuth/oauth instructions.md',
'API Swagger Models/swagger-model.json',
'Guides and FAQs/getting-started.txt',
];
validRefsWithSpaces.forEach(ref => {
const result = validators.validateDocumentReference(ref);
expect(result.isValid).toBe(true);
expect(result.sanitizedValue).toBe(ref);
});
});
});
describe('Max Results Validation', () => {
it('should accept valid numbers', () => {
const result = validators.validateMaxResults(10);
expect(result.isValid).toBe(true);
expect(result.sanitizedValue).toBe(10);
});
it('should use default for undefined', () => {
const result = validators.validateMaxResults(undefined);
expect(result.isValid).toBe(true);
expect(result.sanitizedValue).toBe(5);
});
it('should reject values outside range', () => {
const tooSmall = validators.validateMaxResults(0);
expect(tooSmall.isValid).toBe(false);
expect(tooSmall.errors).toContain('maxResults too small (minimum 1)');
const tooLarge = validators.validateMaxResults(200);
expect(tooLarge.isValid).toBe(false);
expect(tooLarge.errors).toContain('maxResults too large (maximum 100)');
});
});
});
describe('Output Encoding', () => {
describe('JSON Encoding', () => {
it('should encode JSON special characters', () => {
const dangerous = 'value with "quotes" and \n newlines';
const encoded = encoders.encodeJson(dangerous);
expect(encoded).toBe('value with \\"quotes\\" and \\n newlines');
});
it('should handle null bytes', () => {
const withNullByte = 'text\u0000withnull';
const encoded = encoders.encodeJson(withNullByte);
expect(encoded).toBe('text\\u0000withnull');
});
});
describe('Response Sanitization', () => {
it('should sanitize nested objects', () => {
const maliciousResponse = {
results: [
{
text: '<script>alert("xss")</script>',
score: 0.9,
documentReference: 'safe-doc.md',
},
],
};
const sanitized = encoders.sanitizeApiResponse(maliciousResponse);
expect(typeof sanitized).toBe('object');
if (sanitized && typeof sanitized === 'object' && 'results' in sanitized) {
const results = sanitized.results as any[];
expect(results[0].text).toContain('<script>');
}
});
it('should remove control characters', () => {
const withControlChars = 'text\u0001with\u0002control\u0003chars';
const sanitized = encoders.sanitizeApiResponse(withControlChars);
expect(sanitized).toBe('textwithcontrolchars');
});
it('should preserve JSON formatting in mixed content', () => {
const jsonContent = '{"swagger": "2.0", "info": {"title": "Amazon Business API", "version": "2020-08-26"}}';
const sanitized = encoders.sanitizeApiResponse(jsonContent);
// Should preserve quotes and not encode them as "
expect(sanitized).toContain('"swagger"');
expect(sanitized).toContain('"2.0"');
expect(sanitized).not.toContain('"');
});
it('should preserve document references with slashes', () => {
const response = {
results: [
{
text: 'Some documentation content',
score: 0.95,
documentReference: 'Product Search API/product-search-model-search-products.md'
}
]
};
const sanitized = encoders.sanitizeApiResponse(response) as any;
// Should preserve slashes and not encode them as /
expect(sanitized.results[0].documentReference).toBe('Product Search API/product-search-model-search-products.md');
expect(sanitized.results[0].documentReference).not.toContain('/');
});
it('should handle mixed JSON and text content safely', () => {
const mixedContent = `Here is some documentation about the API:
{
"swagger": "2.0",
"info": {
"description": "This is the swagger model for Product Search APIs",
"title": "Amazon Business API for Products"
},
"paths": {
"/products/2020-08-26/products": {
"get": {
"parameters": [
{
"name": "keywords",
"required": false,
"type": "string"
}
]
}
}
}
}
This API allows you to search for products.`;
const sanitized = encoders.sanitizeApiResponse(mixedContent) as string;
// Should preserve JSON quotes
expect(sanitized).toContain('"swagger"');
expect(sanitized).toContain('"2.0"');
expect(sanitized).not.toContain('"swagger"');
// But should still encode dangerous HTML
const dangerousMixed = 'JSON: {"test": "value"} with <script>alert("xss")</script>';
const sanitizedDangerous = encoders.sanitizeApiResponse(dangerousMixed) as string;
expect(sanitizedDangerous).toContain('<script>');
expect(sanitizedDangerous).toContain('"test"'); // JSON preserved
});
});
describe('Error Message Sanitization', () => {
it('should sanitize error messages', () => {
const error = new Error('<script>alert("xss")</script>');
const sanitized = encoders.sanitizeErrorMessage(error);
expect(sanitized).toContain('<script>');
});
it('should redact sensitive information', () => {
const testCases = [
{
input: 'Error with credit card 1234-5678-9012-3456',
shouldContain: '[REDACTED]'
},
{
input: 'Failed for user@example.com',
shouldContain: '[EMAIL]'
},
{
input: 'Connection to 192.168.1.1 failed',
shouldContain: '[IP]'
},
{
input: 'Password123 is invalid',
shouldContain: '[CREDENTIAL]'
},
];
testCases.forEach(({ input, shouldContain }) => {
const sanitized = encoders.sanitizeErrorMessage(input);
expect(sanitized).toContain(shouldContain);
});
});
});
});
describe('Security Integration Tests', () => {
it('should provide convenient processing functions', () => {
expect(typeof processing.validate.query).toBe('function');
expect(typeof processing.encode.json).toBe('function');
expect(typeof processing.response.createError).toBe('function');
});
it('should validate search input comprehensively', () => {
const result = processing.validate.searchInput({
query: '<script>alert("xss")</script>',
maxResults: 5,
});
expect(result.isValid).toBe(false);
expect(result.errors.length).toBeGreaterThan(0);
});
it('should create error responses', () => {
const errorResponse = processing.response.createError('Test error');
expect(errorResponse.isError).toBe(true);
expect(errorResponse.content).toHaveLength(1);
expect(errorResponse.content[0].type).toBe('text');
});
});
describe('Edge Cases and Attack Vectors', () => {
it('should handle Unicode normalization attacks', () => {
// Using Unicode normalization to bypass filters
const unicodeAttack = '\u003cscript\u003ealert\u0028\u0022xss\u0022\u0029\u003c\u002fscript\u003e';
const result = validators.validateQuery(unicodeAttack);
expect(result.isValid).toBe(false);
});
it('should handle mixed encoding attacks', () => {
const mixedAttack = 'java%73cript:alert("xss")';
const result = validators.validateQuery(mixedAttack);
expect(result.isValid).toBe(false);
});
it('should handle polyglot attacks', () => {
const polyglot = 'javascript:/*--></title></style></textarea></script></xmp><svg/onload=alert(/malicious/)>';
const result = validators.validateQuery(polyglot);
expect(result.isValid).toBe(false);
});
it('should handle double encoding attempts', () => {
const doubleEncoded = '%253Cscript%253E'; // Double encoded <script>
const result = validators.validateDocumentReference(doubleEncoded);
expect(result.isValid).toBe(false);
});
});
});