UNPKG

edge-utils

Version:

Platform-agnostic utilities for edge computing and serverless environments

3 lines (2 loc) 10.1 kB
const e=require("crypto"),t=require("ajv");module.exports={SecurityHeadersManager:class{constructor(e={}){this.options={contentSecurityPolicy:e.contentSecurityPolicy||{},hsts:e.hsts||{maxAge:31536e3,includeSubDomains:!0},frameOptions:e.frameOptions||{action:"DENY"},contentTypeOptions:"nosniff",referrerPolicy:e.referrerPolicy||"strict-origin-when-cross-origin",permissionsPolicy:e.permissionsPolicy||{},coop:e.coop||"same-origin",coep:e.coep||"require-corp",...e}}generate(e={}){const t={};!1!==this.options.contentSecurityPolicy.enabled&&(t["Content-Security-Policy"]=this._generateCSP(e.csp)),!1!==this.options.hsts.enabled&&(t["Strict-Transport-Security"]=this._generateHSTS()),!1!==this.options.frameOptions.enabled&&(t["X-Frame-Options"]=this._generateFrameOptions()),t["X-Content-Type-Options"]=this.options.contentTypeOptions,t["Referrer-Policy"]=this.options.referrerPolicy,Object.keys(this.options.permissionsPolicy).length>0&&(t["Permissions-Policy"]=this._generatePermissionsPolicy()),this.options.coep&&(t["Cross-Origin-Embedder-Policy"]=this.options.coep),this.options.coop&&(t["Cross-Origin-Opener-Policy"]=this.options.coop);const{csp:s,...r}=e;return{...t,...r}}_generateCSP(e={}){const t={...this.options.contentSecurityPolicy,...e},s=[];return s.push(`default-src ${t["default-src"]||"'self'"}`),s.push(`script-src ${t["script-src"]||"'self'"}`),s.push(`style-src ${t["style-src"]||"'self' 'unsafe-inline'"}`),s.push(`img-src ${t["img-src"]||"'self' data: https:"}`),s.push(`font-src ${t["font-src"]||"'self'"}`),s.push(`connect-src ${t["connect-src"]||"'self'"}`),s.push(`media-src ${t["media-src"]||"'self'"}`),s.push(`object-src ${t["object-src"]||"'none'"}`),s.push(`frame-src ${t["frame-src"]||"'none'"}`),t.nonce&&s.push(`script-src 'self' 'nonce-${t.nonce}'`),s.join("; ")}_generateHSTS(){const e=this.options.hsts;let t=`max-age=${e.maxAge}`;return e.includeSubDomains&&(t+="; includeSubDomains"),e.preload&&(t+="; preload"),t}_generateFrameOptions(){const e=this.options.frameOptions;return"ALLOW-FROM"===e.action?`ALLOW-FROM ${e.origin}`:e.action||"DENY"}_generatePermissionsPolicy(){const e=[];for(const[t,s]of Object.entries(this.options.permissionsPolicy))Array.isArray(s)?e.push(`${t}=(${s.join(" ")})`):e.push(`${t}=${s}`);return e.join(", ")}static generateNonce(){return e.randomBytes(16).toString("base64")}},CSRFProtection:class{constructor(t={}){this.secret=t.secret||e.randomBytes(32).toString("hex"),this.cookieName=t.cookieName||"csrf-token",this.headerName=t.headerName||"x-csrf-token",this.sameSite=t.sameSite||"strict",this.secure=!1!==t.secure,this.httpOnly=!1!==t.httpOnly}generateToken(t){const s=`${t}:${Date.now()}`,r=e.createHmac("sha256",this.secret).update(s).digest("hex");return Buffer.from(`${s}:${r}`).toString("base64")}verifyToken(t,s){try{const r=Buffer.from(t,"base64").toString(),[n,i,o]=r.split(":");if(n!==s)return!1;if(Date.now()-parseInt(i)>3e5)return!1;const a=`${n}:${i}`,c=e.createHmac("sha256",this.secret).update(a).digest("hex");return e.timingSafeEqual(Buffer.from(o,"hex"),Buffer.from(c,"hex"))}catch(e){return!1}}getCookieOptions(){return{name:this.cookieName,value:"",httpOnly:this.httpOnly,secure:this.secure,sameSite:this.sameSite,path:"/"}}middleware(){return(e,t)=>{const s=e.method?.toUpperCase();if(["GET","HEAD","OPTIONS"].includes(s))return null;const r=e.headers?.[this.headerName.toLowerCase()]||e.headers?.["x-xsrf-token"];if(!r)return new Response("CSRF token missing",{status:403,headers:{"Content-Type":"text/plain"}});const n=t.session?.id||"anonymous";return this.verifyToken(r,n)?null:new Response("CSRF token invalid",{status:403,headers:{"Content-Type":"text/plain"}})}}},XSSPrevention:class{constructor(e={}){this.sanitizationRules=e.sanitizationRules||{},this.outputEncoding=e.outputEncoding||{}}sanitize(e,t="html"){if("string"!=typeof e)return e;let s=e;s=s.replace(/\0/g,"");const r=this.sanitizationRules[t]||[];for(const e of r)s=s.replace(e.pattern,e.replacement);switch(t){case"html":s=this._sanitizeHTML(s);break;case"javascript":s=this._sanitizeJavaScript(s);break;case"url":s=this._sanitizeURL(s)}return s}encode(e,t="html"){if("string"!=typeof e)return e;switch(t){case"html":return e.replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;").replace(/'/g,"&#x27;").replace(/\//g,"&#x2F;");case"javascript":return e.replace(/\\/g,"\\\\").replace(/'/g,"\\'").replace(/"/g,'\\"').replace(/\n/g,"\\n").replace(/\r/g,"\\r").replace(/\t/g,"\\t");case"url":return encodeURIComponent(e);default:return e}}validateContentType(e,t){if(!e||!t)return!1;const s=e.toLowerCase();if(s.includes("json"))try{return JSON.parse(t.toString()),!0}catch(e){return!1}if(s.includes("xml")){return/^<\?xml.*?\?>.*<.*>.*<\/.*>$/s.test(t.toString())}return!0}detectScriptInjection(e){if("string"!=typeof e)return!1;return[/<script[^>]*>.*?<\/script>/gi,/javascript:/gi,/vbscript:/gi,/onload\s*=/gi,/onerror\s*=/gi,/eval\s*\(/gi].some(t=>t.test(e))}_sanitizeHTML(e){return e=(e=(e=e.replace(/<script[^>]*>.*?<\/script>/gis,"")).replace(/on\w+\s*=\s*"[^"]*"/gi,"")).replace(/on\w+\s*=\s*'[^']*'/gi,"")}_sanitizeJavaScript(e){return e=(e=(e=(e=e.replace(/\beval\s*\(/gi,"")).replace(/\bFunction\s*\(/gi,"")).replace(/\bsetTimeout\s*\(/gi,"")).replace(/\bsetInterval\s*\(/gi,"")}_sanitizeURL(e){return e=(e=e.replace(/^javascript:/i,"")).replace(/^vbscript:/i,"")}},RequestValidator:class{constructor(e={}){this.ajv=new t(e.ajvOptions||{}),this.schemas=new Map,this.maxBodySize=e.maxBodySize||10485760,this.allowedContentTypes=e.allowedContentTypes||["application/json","application/x-www-form-urlencoded","multipart/form-data","text/plain"]}addSchema(e,t){this.schemas.set(e,this.ajv.compile(t))}validate(e,t){const s=this.schemas.get(t);if(!s)return{valid:!1,errors:[{message:`Schema not found: ${t}`}]};return{valid:s({method:e.method,url:e.url,headers:e.headers,query:this._parseQuery(e.url),body:e.body}),errors:s.errors||[]}}validateJSON(e,t){const s=this.ajv.compile(t);return{valid:s(e),errors:s.errors||[]}}validateQuery(e,t){const s=[];for(const[r,n]of Object.entries(t)){const t=e[r];if(n.required&&null==t)s.push({field:r,message:"Required field missing"});else if(null!=t){if("string"===n.type&&"string"!=typeof t)s.push({field:r,message:"Must be a string"});else if("number"===n.type){const e=Number(t);isNaN(e)?s.push({field:r,message:"Must be a number"}):void 0!==n.min&&e<n.min?s.push({field:r,message:`Must be >= ${n.min}`}):void 0!==n.max&&e>n.max&&s.push({field:r,message:`Must be <= ${n.max}`})}else"boolean"===n.type&&"boolean"!=typeof t&&"true"!==t&&"false"!==t&&s.push({field:r,message:"Must be a boolean"});if(n.pattern&&"string"==typeof t){new RegExp(n.pattern).test(t)||s.push({field:r,message:"Invalid format"})}n.sanitize&&"string"==typeof t&&(e[r]=t.trim())}}return{valid:0===s.length,errors:s,sanitized:e}}validatePath(e,t){const s=[];for(const[r,n]of Object.entries(t)){const t=e[r];if(n.required&&null==t)s.push({field:r,message:"Required parameter missing"});else if(null!=t&&n.pattern){new RegExp(`^${n.pattern}$`).test(t)||s.push({field:r,message:"Invalid parameter format"})}}return{valid:0===s.length,errors:s}}validateHeaders(e,t){const s=[],r={};for(const[n,i]of Object.entries(t)){const t=e[n.toLowerCase()];if(i.required&&null==t)s.push({field:n,message:"Required header missing"});else if(null!=t&&(i.normalize?r[n]=t.trim().toLowerCase():r[n]=t,i.pattern)){new RegExp(i.pattern).test(t)||s.push({field:n,message:"Invalid header format"})}}return{valid:0===s.length,errors:s,normalized:r}}validateBody(e){const t=[],s=e.headers?.["content-type"];if(s&&!this.allowedContentTypes.some(e=>s.toLowerCase().includes(e))&&t.push({message:`Unsupported content type: ${s}`}),e.body){const s=Buffer.isBuffer(e.body)?e.body.length:JSON.stringify(e.body).length;s>this.maxBodySize&&t.push({message:`Body too large: ${s} > ${this.maxBodySize}`})}return{valid:0===t.length,errors:t}}_parseQuery(e){try{const t=new URL(e),s={};for(const[e,r]of t.searchParams)s[e]=r;return s}catch(e){return{}}}},DDoSProtection:class{constructor(e={}){this.requestHistory=new Map,this.spikeThreshold=e.spikeThreshold||100,this.blockDuration=e.blockDuration||9e5,this.blockedIPs=new Set,this.patternAnalysis=!1!==e.patternAnalysis,this.challengeEnabled=!1!==e.challengeEnabled}analyze(e){const t=this._getClientIP(e),s=Date.now();if(this.blockedIPs.has(t))return{suspicious:!0,block:!0,challenge:!1};this.requestHistory.has(t)||this.requestHistory.set(t,[]);const r=this.requestHistory.get(t);r.push(s);const n=s-3e5,i=r.filter(e=>e>n);this.requestHistory.set(t,i);const o=i.length>this.spikeThreshold;let a=!1;this.patternAnalysis&&(a=this._analyzePatterns(e,i));return o||a?(this.blockedIPs.add(t),setTimeout(()=>{this.blockedIPs.delete(t)},this.blockDuration),{suspicious:!0,block:!0,challenge:!1}):a&&this.challengeEnabled?{suspicious:!0,block:!1,challenge:!0}:{suspicious:!1,block:!1,challenge:!1}}generateChallenge(){const t=e.randomBytes(16).toString("hex");return new Response(`\n<!DOCTYPE html>\n<html>\n<head>\n <title>Security Challenge</title>\n</head>\n<body>\n <h1>Please verify you are human</h1>\n <form method="POST">\n <input type="hidden" name="challenge_id" value="${t}">\n <button type="submit">Continue</button>\n </form>\n <script>\n // Simple JavaScript challenge\n window.challengeCompleted = false;\n setTimeout(() => { window.challengeCompleted = true; }, 2000);\n <\/script>\n</body>\n</html>`,{status:403,headers:{"Content-Type":"text/html","X-Challenge-ID":t}})}_getClientIP(e){return e.headers?.["cf-connecting-ip"]||e.headers?.["x-forwarded-for"]?.split(",")[0]||e.headers?.["x-real-ip"]||"127.0.0.1"}_analyzePatterns(e,t){if(t.length>=10){const e=[];for(let s=1;s<t.length;s++)e.push(t[s]-t[s-1]);const s=e.reduce((e,t)=>e+t,0)/e.length;if(e.reduce((e,t)=>e+Math.pow(t-s,2),0)/e.length<1e3)return!0}return!1}middleware(){return(e,t)=>{const s=this.analyze(e);return s.block?new Response("Access denied - potential DDoS attack detected",{status:403,headers:{"Content-Type":"text/plain"}}):s.challenge?this.generateChallenge():null}}}}; //# sourceMappingURL=security.esm.js.map