Преглед изворни кода

Added a security check when sending an e-mail

Brendan Abolivier пре 8 година
родитељ
комит
b52093834b
Signed by: Brendan Abolivier <contact@brendanabolivier.com> GPG key ID: 8EF1500759F70623
3 измењених фајлова са 137 додато и 5 уклоњено
  1. 25
    1
      front/form.js
  2. 1
    1
      package.json
  3. 111
    3
      server.js

+ 25
- 1
front/form.js Прегледај датотеку

@@ -8,6 +8,14 @@ var items = {
8 8
 var server  = getServer();
9 9
 var xhrSend = new XMLHttpRequest();
10 10
 
11
+var token = "";
12
+var xhrToken = new XMLHttpRequest();
13
+xhrToken.onreadystatechange = function() {
14
+    if(xhrToken.readyState == XMLHttpRequest.DONE) {
15
+        token = xhrToken.responseText;
16
+    }
17
+}
18
+
11 19
 
12 20
 // Returns the server's base URI based on the user's script tag
13 21
 // return: the SMAM server's base URI
@@ -76,6 +84,10 @@ function generateForm(id) {
76 84
             }
77 85
         }
78 86
     };
87
+    
88
+    // Retrieve the token from the server
89
+    
90
+    getToken();
79 91
 }
80 92
 
81 93
 
@@ -170,6 +182,9 @@ function sendForm() {
170 182
     xhrSend.open('POST', server + '/send');
171 183
     xhrSend.setRequestHeader('Content-Type', 'application/json');
172 184
     xhrSend.send(JSON.stringify(getFormData()));
185
+    
186
+    // Get a new token
187
+    getToken();
173 188
 }
174 189
 
175 190
 
@@ -180,7 +195,8 @@ function getFormData() {
180 195
         name: document.getElementById(items.name + '_input').value,
181 196
         addr: document.getElementById(items.addr + '_input').value,
182 197
         subj: document.getElementById(items.subj + '_input').value,
183
-        text: document.getElementById(items.text + '_textarea').value
198
+        text: document.getElementById(items.text + '_textarea').value,
199
+        token: token
184 200
     }
185 201
 }
186 202
 
@@ -192,4 +208,12 @@ function cleanForm() {
192 208
     document.getElementById(items.addr + '_input').value = '';
193 209
     document.getElementById(items.subj + '_input').value = '';
194 210
     document.getElementById(items.text + '_textarea').value = '';
211
+}
212
+
213
+
214
+// Ask the server for a token
215
+// return: nothing
216
+function getToken() {
217
+    xhrToken.open('GET', server + '/register');
218
+    xhrToken.send();
195 219
 }

+ 1
- 1
package.json Прегледај датотеку

@@ -1,6 +1,6 @@
1 1
 {
2 2
   "name": "smam",
3
-  "version": "0.0.1",
3
+  "version": "1.1.0",
4 4
   "description": "SMAM (short for Send Me A Mail) is an free (as in freedom) contact form embedding software.",
5 5
   "contributors": [
6 6
     {

+ 111
- 3
server.js Прегледај датотеку

@@ -1,5 +1,6 @@
1 1
 var pug         = require('pug');
2 2
 var nodemailer  = require('nodemailer');
3
+var crypto      = require('crypto');
3 4
 var settings    = require('./settings');
4 5
 
5 6
 // Web server
@@ -19,6 +20,10 @@ var log = printit({
19 20
 var transporter = nodemailer.createTransport(settings.mailserver);
20 21
 
21 22
 
23
+// Verification tokens
24
+var tokens = {};
25
+
26
+
22 27
 // Serve static (JS + HTML) files
23 28
 app.use(express.static('front'));
24 29
 // Body parsing
@@ -26,8 +31,40 @@ app.use(bodyParser.urlencoded({ extended: true }));
26 31
 app.use(bodyParser.json());
27 32
 
28 33
 
34
+// A request on /register generates a token and store it, along the user's
35
+// address, on the tokens object
36
+app.get('/register', function(req, res, next) {
37
+    // Get IP from express
38
+    let ip = req.headers['x-forwarded-for'] || req.connection.remoteAddress;
39
+    if(tokens[ip] === undefined) {
40
+        tokens[ip] = [];
41
+    }
42
+    // Generate token
43
+    crypto.randomBytes(10, (err, buf) => { 
44
+        let token = buf.toString('hex');
45
+        // Store and send the token
46
+        tokens[ip].push({
47
+            token: token,
48
+            // A token expires after 12h
49
+            expire: new Date().getTime() + 12 * 3600 * 1000
50
+        });
51
+        res.status(200).send(token);
52
+    });
53
+});
54
+
55
+
29 56
 // A request on /send with user input = mail to be sent
30 57
 app.post('/send', function(req, res, next) {
58
+    if(!checkBody(req.body)) {
59
+        return res.status(400).send();
60
+    }
61
+    
62
+    let ip = req.headers['x-forwarded-for'] || req.connection.remoteAddress;
63
+    
64
+    if(!checkToken(ip, req.body.token)) {
65
+        return res.status(403).send();
66
+    }
67
+    
31 68
     // Count the failures
32 69
     let status = {
33 70
         failed: 0,
@@ -53,13 +90,12 @@ app.post('/send', function(req, res, next) {
53 90
     // Send the email to all users
54 91
     sendMails(params, function(err, infos) {
55 92
         if(err) {
56
-            log.error(err)
93
+            log.error(err);
57 94
         }
58 95
         logStatus(infos);
59 96
     }, function() {
60
-        res.header('Access-Control-Allow-Origin', '*');
61 97
         if(status.failed === status.total) {
62
-            res.status(500).send()
98
+            res.status(500).send();
63 99
         } else {
64 100
             res.status(200).send();
65 101
         }
@@ -75,6 +111,10 @@ app.listen(port, function() {
75 111
 });
76 112
 
77 113
 
114
+// Run the clean every hour
115
+var tokensChecks = setTimeout(cleanTokens, 3600 * 1000);
116
+
117
+
78 118
 // Send mails to the recipients specified in the JSON settings file
79 119
 // content: object containing mail params
80 120
 //  {
@@ -117,4 +157,72 @@ function logStatus(infos) {
117 157
         status.failed++;
118 158
         log.info('Message failed to send to ' + infos.rejected[0]);
119 159
     }
160
+}
161
+
162
+
163
+// Checks if the request's sender has been registered (and unregister it if not)
164
+// ip: sender's IP address
165
+// token: token used by the sender
166
+// return: true if the user was registered, false else
167
+function checkToken(ip, token) {
168
+    let verified = false;
169
+    
170
+    // Check if there's at least one token for this IP
171
+    if(tokens[ip] !== undefined) {
172
+        if(tokens[ip].length !== 0) {
173
+            // There's at least one element for this IP, let's check the tokens
174
+            for(var i = 0; i < tokens[ip].length; i++) {
175
+                if(!tokens[ip][i].token.localeCompare(token)) {
176
+                    // We found the right token
177
+                    verified = true;
178
+                    // Removing the token
179
+                    tokens[ip].pop(tokens[ip][i]);
180
+                    break;
181
+                }
182
+            }
183
+        } 
184
+    }
185
+    
186
+    if(!verified) {
187
+        log.warn(ip + ' just tried to send a message with an invalid token');
188
+    }
189
+    
190
+    return verified;
191
+}
192
+
193
+
194
+// Checks if all the required fields are in the request body
195
+// body: body taken from express's request object
196
+// return: true if the body is valid, false else
197
+function checkBody(body) {
198
+    let valid = false;
199
+    
200
+    if(body.token !== undefined && body.subj !== undefined 
201
+        && body.name !== undefined && body.addr !== undefined 
202
+        && body.text !== undefined) {
203
+        valid = true;
204
+    }
205
+    
206
+    return valid;
207
+}
208
+
209
+
210
+// Checks the tokens object to see if no token has expired
211
+// return: nothing
212
+function cleanTokens() {
213
+    // Get current time for comparison
214
+    let now = new Date().getTime();
215
+    
216
+    for(let ip in tokens) { // Check for each IP in the object
217
+        for(let token of tokens[ip]) { // Check for each token of an IP
218
+            if(token.expire < now) { // Token has expired
219
+                tokens[ip].pop(token);
220
+            }
221
+        }
222
+        if(tokens[ip].length === 0) { // No more element for this IP
223
+            delete tokens[ip];
224
+        }
225
+    }
226
+    
227
+    log.info('Cleared expired tokens');
120 228
 }