Browse Source

Added a security check when sending an e-mail

Brendan Abolivier 8 years ago
parent
commit
b52093834b
Signed by: Brendan Abolivier <contact@brendanabolivier.com> GPG key ID: 8EF1500759F70623
3 changed files with 137 additions and 5 deletions
  1. 25
    1
      front/form.js
  2. 1
    1
      package.json
  3. 111
    3
      server.js

+ 25
- 1
front/form.js View File

8
 var server  = getServer();
8
 var server  = getServer();
9
 var xhrSend = new XMLHttpRequest();
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
 // Returns the server's base URI based on the user's script tag
20
 // Returns the server's base URI based on the user's script tag
13
 // return: the SMAM server's base URI
21
 // return: the SMAM server's base URI
76
             }
84
             }
77
         }
85
         }
78
     };
86
     };
87
+    
88
+    // Retrieve the token from the server
89
+    
90
+    getToken();
79
 }
91
 }
80
 
92
 
81
 
93
 
170
     xhrSend.open('POST', server + '/send');
182
     xhrSend.open('POST', server + '/send');
171
     xhrSend.setRequestHeader('Content-Type', 'application/json');
183
     xhrSend.setRequestHeader('Content-Type', 'application/json');
172
     xhrSend.send(JSON.stringify(getFormData()));
184
     xhrSend.send(JSON.stringify(getFormData()));
185
+    
186
+    // Get a new token
187
+    getToken();
173
 }
188
 }
174
 
189
 
175
 
190
 
180
         name: document.getElementById(items.name + '_input').value,
195
         name: document.getElementById(items.name + '_input').value,
181
         addr: document.getElementById(items.addr + '_input').value,
196
         addr: document.getElementById(items.addr + '_input').value,
182
         subj: document.getElementById(items.subj + '_input').value,
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
     document.getElementById(items.addr + '_input').value = '';
208
     document.getElementById(items.addr + '_input').value = '';
193
     document.getElementById(items.subj + '_input').value = '';
209
     document.getElementById(items.subj + '_input').value = '';
194
     document.getElementById(items.text + '_textarea').value = '';
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 View File

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

+ 111
- 3
server.js View File

1
 var pug         = require('pug');
1
 var pug         = require('pug');
2
 var nodemailer  = require('nodemailer');
2
 var nodemailer  = require('nodemailer');
3
+var crypto      = require('crypto');
3
 var settings    = require('./settings');
4
 var settings    = require('./settings');
4
 
5
 
5
 // Web server
6
 // Web server
19
 var transporter = nodemailer.createTransport(settings.mailserver);
20
 var transporter = nodemailer.createTransport(settings.mailserver);
20
 
21
 
21
 
22
 
23
+// Verification tokens
24
+var tokens = {};
25
+
26
+
22
 // Serve static (JS + HTML) files
27
 // Serve static (JS + HTML) files
23
 app.use(express.static('front'));
28
 app.use(express.static('front'));
24
 // Body parsing
29
 // Body parsing
26
 app.use(bodyParser.json());
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
 // A request on /send with user input = mail to be sent
56
 // A request on /send with user input = mail to be sent
30
 app.post('/send', function(req, res, next) {
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
     // Count the failures
68
     // Count the failures
32
     let status = {
69
     let status = {
33
         failed: 0,
70
         failed: 0,
53
     // Send the email to all users
90
     // Send the email to all users
54
     sendMails(params, function(err, infos) {
91
     sendMails(params, function(err, infos) {
55
         if(err) {
92
         if(err) {
56
-            log.error(err)
93
+            log.error(err);
57
         }
94
         }
58
         logStatus(infos);
95
         logStatus(infos);
59
     }, function() {
96
     }, function() {
60
-        res.header('Access-Control-Allow-Origin', '*');
61
         if(status.failed === status.total) {
97
         if(status.failed === status.total) {
62
-            res.status(500).send()
98
+            res.status(500).send();
63
         } else {
99
         } else {
64
             res.status(200).send();
100
             res.status(200).send();
65
         }
101
         }
75
 });
111
 });
76
 
112
 
77
 
113
 
114
+// Run the clean every hour
115
+var tokensChecks = setTimeout(cleanTokens, 3600 * 1000);
116
+
117
+
78
 // Send mails to the recipients specified in the JSON settings file
118
 // Send mails to the recipients specified in the JSON settings file
79
 // content: object containing mail params
119
 // content: object containing mail params
80
 //  {
120
 //  {
117
         status.failed++;
157
         status.failed++;
118
         log.info('Message failed to send to ' + infos.rejected[0]);
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
 }