diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5e7ecf6..267019a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -6,6 +6,10 @@ on: pull_request: branches: [main, master] +# Restrict permissions to minimum required (security best practice) +permissions: + contents: read + jobs: test: runs-on: ubuntu-latest diff --git a/scripts/benchmark.js b/scripts/benchmark.js index 798d52a..1a674a9 100755 --- a/scripts/benchmark.js +++ b/scripts/benchmark.js @@ -7,10 +7,21 @@ import { UrbanKnowledgeMapper } from '../src/mapper.js'; import { PerformanceMonitor, benchmark } from '../src/performance.js'; -import { randomUUID } from 'crypto'; +import { randomUUID, randomBytes } from 'crypto'; const DATASET_SIZES = [10, 50, 100, 500, 1000]; +/** + * Generate a cryptographically secure random float between 0 and 1 + * @returns {number} Random float in range [0, 1) + */ +function secureRandomFloat() { + // Use 4 bytes to create a 32-bit unsigned integer, then normalize to [0, 1) + const bytes = randomBytes(4); + const uint32 = bytes.readUInt32BE(0); + return uint32 / 0x100000000; +} + async function generateTestData(count) { const mapper = new UrbanKnowledgeMapper('/tmp/benchmark-ubicity'); await mapper.initialize(); @@ -26,8 +37,8 @@ async function generateTestData(count) { location: { name: locations[i % locations.length], coordinates: { - latitude: 37.7749 + (Math.random() - 0.5) * 0.1, - longitude: -122.4194 + (Math.random() - 0.5) * 0.1, + latitude: 37.7749 + (secureRandomFloat() - 0.5) * 0.1, + longitude: -122.4194 + (secureRandomFloat() - 0.5) * 0.1, }, }, }, diff --git a/src/privacy.js b/src/privacy.js index dd4bc35..6ade38d 100644 --- a/src/privacy.js +++ b/src/privacy.js @@ -207,11 +207,16 @@ function hashString(str) { * @returns {string} Sanitized text */ function sanitizeText(text) { - let sanitized = text; - - // Email addresses + // Limit input length to prevent ReDoS attacks + const MAX_TEXT_LENGTH = 10000; + let sanitized = text.length > MAX_TEXT_LENGTH + ? text.slice(0, MAX_TEXT_LENGTH) + : text; + + // Email addresses - use possessive-like matching to prevent backtracking + // Pattern: local part (alphanumeric, limited special chars) @ domain sanitized = sanitized.replace( - /[\w.-]+@[\w.-]+\.\w+/g, + /[a-zA-Z0-9](?:[a-zA-Z0-9._-]{0,62}[a-zA-Z0-9])?@[a-zA-Z0-9](?:[a-zA-Z0-9.-]{0,252}[a-zA-Z0-9])?\.[a-zA-Z]{2,63}/g, '[email]' ); @@ -221,9 +226,9 @@ function sanitizeText(text) { '[phone]' ); - // URLs + // URLs - use non-greedy matching with explicit character class sanitized = sanitized.replace( - /https?:\/\/[^\s]+/g, + /https?:\/\/[^\s]{1,2000}/g, '[url]' );