Sfoglia il codice sorgente

Merge branch 'main' of https://github.com/HeyPuter/puter into main

Nariman Jelveh 1 anno fa
parent
commit
3be7af8fb8

+ 3 - 0
packages/backend/src/CoreModule.js

@@ -192,6 +192,9 @@ const install = async ({ services, app }) => {
 
     const { SessionService } = require('./services/SessionService');
     services.registerService('session', SessionService);
+
+    const { EdgeRateLimitService } = require('./services/abuse-prevention/EdgeRateLimitService');
+    services.registerService('edge-rate-limit', EdgeRateLimitService);
 }
 
 const install_legacy = async ({ services }) => {

+ 10 - 0
packages/backend/src/routers/change_email.js

@@ -51,6 +51,11 @@ const CHANGE_EMAIL_START = eggspress('/change_email/start', {
             key: 'new_email', expected: 'a valid email address' });
     }
 
+    const svc_edgeRateLimit = req.services.get('edge-rate-limit');
+    if ( ! svc_edgeRateLimit.check('change-email-start') ) {
+        return res.status(429).send('Too many requests.');
+    }
+
     // check if email is already in use
     const db = req.services.get('database').get(DB_WRITE, 'auth');
     const rows = await db.read(
@@ -93,6 +98,11 @@ const CHANGE_EMAIL_CONFIRM = eggspress('/change_email/confirm', {
         throw APIError.create('field_missing', null, { key: 'token' });
     }
 
+    const svc_edgeRateLimit = req.services.get('edge-rate-limit');
+    if ( ! svc_edgeRateLimit.check('change-email-confirm') ) {
+        return res.status(429).send('Too many requests.');
+    }
+
     const { token, user_id } = jwt.verify(jwt_token, config.jwt_secret);
 
     const db = req.services.get('database').get(DB_WRITE, 'auth');

+ 3 - 4
packages/backend/src/routers/login.js

@@ -60,11 +60,10 @@ router.post('/login', express.json(), body_parser_error_handler, async (req, res
     else if(req.body.email && !validator.isEmail(req.body.email))
         return res.status(400).send('Invalid email.')
 
-    // Increment & check rate limit
-    if(kv.incr(`login|${req.ip}|${req.body.email ?? req.body.username}`) > 10)
+    const svc_edgeRateLimit = req.services.get('edge-rate-limit');
+    if ( ! svc_edgeRateLimit.check('login') ) {
         return res.status(429).send('Too many requests.');
-    // Set expiry for rate limit
-    kv.expire(`login|${req.ip}|${req.body.email ?? req.body.username}`, 60*10, 'NX')
+    }
 
     try{
         let user;

+ 5 - 0
packages/backend/src/routers/passwd.js

@@ -45,6 +45,11 @@ router.post('/passwd', auth, express.json(), async (req, res, next)=>{
     else if (typeof req.body.new_pass !== 'string')
         return res.status(400).send('new_pass must be a string.')
 
+    const svc_edgeRateLimit = req.services.get('edge-rate-limit');
+    if ( ! svc_edgeRateLimit.check('passwd') ) {
+        return res.status(429).send('Too many requests.');
+    }
+
     try{
         // check old_pass
         const isMatch = await bcrypt.compare(req.body.old_pass, req.user.password)

+ 5 - 0
packages/backend/src/routers/save_account.js

@@ -70,6 +70,11 @@ router.post('/save_account', auth, express.json(), async (req, res, next)=>{
     else if(req.body.password.length < config.min_pass_length)
         return res.status(400).send(`Password must be at least ${config.min_pass_length} characters long.`)
 
+    const svc_edgeRateLimit = req.services.get('edge-rate-limit');
+    if ( ! svc_edgeRateLimit.check('save-account') ) {
+        return res.status(429).send('Too many requests.');
+    }
+
     // duplicate username check, do this only if user has supplied a new username
     if(req.body.username !== req.user.username && await username_exists(req.body.username))
         return res.status(400).send('This username already exists in our database. Please use another one.');

+ 5 - 0
packages/backend/src/routers/send-confirm-email.js

@@ -27,6 +27,11 @@ const { DB_WRITE } = require('../services/database/consts.js');
 // POST /send-confirm-email
 // -----------------------------------------------------------------------//
 router.post('/send-confirm-email', auth, express.json(), async (req, res, next)=>{
+    const svc_edgeRateLimit = req.services.get('edge-rate-limit');
+    if ( ! svc_edgeRateLimit.check('send-confirm-email') ) {
+        return res.status(429).send('Too many requests.');
+    }
+
     // check subdomain
     if(require('../helpers').subdomain(req) !== 'api')
         next();

+ 6 - 0
packages/backend/src/routers/send-pass-recovery-email.js

@@ -51,6 +51,12 @@ router.post('/send-pass-recovery-email', express.json(), body_parser_error_handl
     else if(req.body.email && !validator.isEmail(req.body.email))
         return res.status(400).send('Invalid email.')
 
+    const svc_edgeRateLimit = req.services.get('edge-rate-limit');
+    if ( ! svc_edgeRateLimit.check('send-pass-recovery-email') ) {
+        return res.status(429).send('Too many requests.');
+    }
+
+
     try{
         let user;
         // see if username exists

+ 5 - 0
packages/backend/src/routers/set-pass-using-token.js

@@ -52,6 +52,11 @@ router.post('/set-pass-using-token', express.json(), async (req, res, next)=>{
     else if(req.body.password.length < config.min_pass_length)
         return res.status(400).send(`Password must be at least ${config.min_pass_length} characters long.`)
 
+    const svc_edgeRateLimit = req.services.get('edge-rate-limit');
+    if ( ! svc_edgeRateLimit.check('set-pass-using-token') ) {
+        return res.status(429).send('Too many requests.');
+    }
+
     try{
         const info = await db.write(
             'UPDATE user SET password=?, pass_recovery_token=NULL WHERE `uuid` = ? AND pass_recovery_token = ?',

+ 5 - 0
packages/backend/src/routers/signup.js

@@ -44,6 +44,11 @@ module.exports = eggspress(['/signup'], {
     if(require('../helpers').subdomain(req) !== 'api' && require('../helpers').subdomain(req) !== '')
         next();
 
+    const svc_edgeRateLimit = req.services.get('edge-rate-limit');
+    if ( ! svc_edgeRateLimit.check('signup') ) {
+        return res.status(429).send('Too many requests.');
+    }
+
     // modules
     const db = req.services.get('database').get(DB_WRITE, 'auth');
     const bcrypt = require('bcrypt')

+ 95 - 0
packages/backend/src/services/abuse-prevention/EdgeRateLimitService.js

@@ -0,0 +1,95 @@
+const { Context } = require("../../util/context");
+const { asyncSafeSetInterval } = require("../../util/promise");
+
+const { MINUTE, HOUR } = require('../../util/time.js');
+const BaseService = require("../BaseService");
+
+class EdgeRateLimitService extends BaseService {
+    _construct () {
+        this.scopes = {
+            ['login']: {
+                limit: 3,
+                window: 15 * MINUTE,
+            },
+            ['signup']: {
+                limit: 10,
+                window: 15 * MINUTE,
+            },
+            ['send-confirm-email']: {
+                limit: 10,
+                window: HOUR,
+            },
+            ['send-pass-recovery-email']: {
+                limit: 10,
+                window: HOUR,
+            },
+            ['set-pass-using-token']: {
+                limit: 10,
+                window: HOUR,
+            },
+            ['save-account']: {
+                limit: 10,
+                window: HOUR,
+            },
+            ['change-email-start']: {
+                limit: 10,
+                window: HOUR,
+            },
+            ['change-email-confirm']: {
+                limit: 10,
+                window: HOUR,
+            },
+            ['passwd']: {
+                limit: 10,
+                window: HOUR,
+            },
+        };
+        this.requests = new Map();
+    }
+
+    async _init () {
+        asyncSafeSetInterval(() => this.cleanup(), 5 * MINUTE);
+    }
+
+    check (scope) {
+        const { window, limit } = this.scopes[scope];
+
+        const requester = Context.get('requester');
+        const rl_identifier = requester.rl_identifier;
+        const key = `${scope}:${rl_identifier}`;
+        const now = Date.now();
+        const windowStart = now - window;
+
+        if (!this.requests.has(key)) {
+            this.requests.set(key, []);
+        }
+
+        // Access the timestamps of past requests for this scope and IP
+        const timestamps = this.requests.get(key);
+
+        // Remove timestamps that are outside the current window
+        while (timestamps.length > 0 && timestamps[0] < windowStart) {
+            timestamps.shift();
+        }
+
+        // Check if the current request exceeds the rate limit
+        if (timestamps.length >= limit) {
+            return false;
+        } else {
+            // Add current timestamp and allow the request
+            timestamps.push(now);
+            return true;
+        }
+    }
+
+    cleanup() {
+        this.log.tick('edge rate-limit cleanup task');
+        for (const [key, timestamps] of this.requests.entries()) {
+            if (timestamps.length === 0) {
+                this.requests.delete(key);
+            }
+        }
+    }
+}
+
+module.exports = { EdgeRateLimitService };

+ 4 - 0
packages/backend/src/services/abuse-prevention/IdentificationService.js

@@ -68,6 +68,10 @@ class Requester {
         return puter_origins.includes(this.origin);
     }
 
+    get rl_identifier () {
+        return this.ip_forwarded || this.ip;
+    }
+
     serialize () {
         return {
             ua: this.ua,