conductor

CI task system
git clone git://git.finwo.net/app/conductor
Log | Files | Refs | README | LICENSE

key.js (1818B)


      1 // src/lib/storage/key.js - object key validation shared by both backends
      2 //
      3 // Keys are slash separated and always relative. Validation is centralised
      4 // because a key frequently originates from a job artifact path, which is
      5 // attacker controlled for any worker or repository that is not fully trusted.
      6 
      7 export class StorageNotFound extends Error {
      8   constructor(key) {
      9     super(`object not found: ${key}`);
     10     this.name = 'StorageNotFound';
     11     this.code = 'ENOENT';
     12     this.key = key;
     13   }
     14 }
     15 
     16 export function assertKey(key) {
     17   if (typeof key !== 'string' || key.length === 0) {
     18     throw new Error('storage key must be a non-empty string');
     19   }
     20   if (key.length > 1024) {
     21     throw new Error(`storage key exceeds 1024 characters: ${key.slice(0, 64)}...`);
     22   }
     23   if (key.startsWith('/')) {
     24     throw new Error(`storage key must be relative: ${key}`);
     25   }
     26   if (key.includes('\\')) {
     27     throw new Error(`storage key must use forward slashes: ${key}`);
     28   }
     29   // A NUL byte truncates the path in some syscalls.
     30   if (key.includes('\0')) {
     31     throw new Error('storage key contains a NUL byte');
     32   }
     33   for (const segment of key.split('/')) {
     34     if (segment === '' || segment === '.' || segment === '..') {
     35       throw new Error(`storage key contains an invalid path segment: ${key}`);
     36     }
     37   }
     38   return key;
     39 }
     40 
     41 // Normalizes a path reported by a worker into a safe key suffix. Returns null
     42 // when nothing usable remains, so the caller can reject the upload.
     43 export function sanitizeRelativePath(input) {
     44   if (typeof input !== 'string') return null;
     45   const parts = input
     46     .replace(/\\/g, '/')
     47     .split('/')
     48     .filter((p) => p !== '' && p !== '.' && p !== '..' && !p.includes('\0'));
     49   if (parts.length === 0) return null;
     50   const joined = parts.join('/');
     51   return joined.length > 512 ? null : joined;
     52 }