2
2
import logging
3
3
from datetime import datetime
4
4
from random import Random
5
+ import requests
5
6
import base64
6
7
7
8
from flask import Flask , render_template , request , jsonify
8
- from flask_recaptcha import ReCaptcha
9
9
from flask_limiter import Limiter
10
10
from flask_limiter .util import get_remote_address
11
11
12
12
from sendgrid import SendGridAPIClient
13
- from sendgrid .helpers .mail import ( Mail , Attachment , FileContent , FileName , FileType , Disposition )
13
+ from sendgrid .helpers .mail import Mail , Attachment , FileContent , FileName , FileType , Disposition
14
14
15
15
from dotenv import load_dotenv
16
16
17
17
load_dotenv ()
18
18
19
19
class Config :
20
- MAX_CONTENT_LENGTH = 15 * 1024 * 1024 # 15 MB
20
+ MAX_CONTENT_LENGTH = 15 * 1024 * 1024 # 15 MB
21
21
EMAIL_DOMAIN = "@ethereum.org"
22
22
DEFAULT_RECIPIENT_EMAIL = "[email protected] "
23
23
NUMBER_OF_ATTACHMENTS = int (os .getenv ('NUMBEROFATTACHMENTS' , 10 ))
24
- DEBUG_MODE = os .getenv ('DEBUG' , 'False' ).lower () == 'true'
25
24
SECRET_KEY = os .getenv ('SECRET_KEY' , 'you-should-set-a-secret-key' )
26
25
27
26
def validate_env_vars (required_vars ):
@@ -81,7 +80,7 @@ def create_email(to_email, identifier, text, all_attachments, reference=''):
81
80
subject = f'Secure Form Submission { identifier } '
82
81
if reference :
83
82
subject = f'{ reference } { subject } '
84
-
83
+
85
84
message = Mail (
86
85
from_email = FROMEMAIL ,
87
86
to_emails = to_email ,
@@ -102,26 +101,27 @@ def create_email(to_email, identifier, text, all_attachments, reference=''):
102
101
message .add_attachment (attachedFile )
103
102
return message
104
103
105
-
106
104
def validate_recaptcha (recaptcha_response ):
107
105
"""
108
- Validates the ReCaptcha response.
106
+ Validates the ReCaptcha response using Google's API .
109
107
"""
110
- try :
111
- if not recaptcha_response :
112
- logging .error ('No ReCaptcha response provided.' )
113
- raise ValueError ('ReCaptcha verification failed: No response provided.' )
108
+ secret_key = os .getenv ('RECAPTCHASECRETKEY' )
109
+ payload = {
110
+ 'secret' : secret_key ,
111
+ 'response' : recaptcha_response
112
+ }
113
+ response = requests .post ('https://www.google.com/recaptcha/api/siteverify' , data = payload )
114
+ result = response .json ()
114
115
115
- # Perform the verification
116
- if not recaptcha .verify (response = recaptcha_response ):
117
- logging .error ('ReCaptcha verification failed for response: %s' , recaptcha_response )
118
- raise ValueError ('ReCaptcha verification failed.' )
116
+ # Log the validation result
117
+ logging .info (f"ReCaptcha validation response: { result } " )
119
118
120
- logging .info ('ReCaptcha verification succeeded for response: %s' , recaptcha_response )
121
- except Exception as e :
122
- logging .error ('Error during ReCaptcha validation: %s' , str (e ))
123
- raise
119
+ if not result .get ('success' ):
120
+ raise ValueError ('ReCaptcha verification failed.' )
124
121
122
+ # Check action and score thresholds for additional security
123
+ if result .get ('score' , 1.0 ) < 0.5 :
124
+ raise ValueError ('ReCaptcha score is too low, indicating potential abuse.' )
125
125
126
126
def send_email (message ):
127
127
"""
@@ -134,13 +134,10 @@ def send_email(message):
134
134
if response .status_code not in [200 , 201 , 202 ]:
135
135
logging .error ('SendGrid failed with status code: %s, response body: %s' , response .status_code , response .body )
136
136
raise ValueError (f"Error: Failed to send email. Status code: { response .status_code } , body: { response .body } " )
137
- else :
138
- logging .info ('Email sent successfully. Status code: %s, response body: %s' , response .status_code , response .body )
139
137
except Exception as e :
140
138
logging .error ('Error sending email via SendGrid: %s' , str (e ))
141
139
raise
142
140
143
-
144
141
# Validate required environment variables
145
142
required_env_vars = ['RECAPTCHASITEKEY' , 'RECAPTCHASECRETKEY' , 'SENDGRIDAPIKEY' , 'SENDGRIDFROMEMAIL' ]
146
143
validate_env_vars (required_env_vars )
@@ -152,40 +149,39 @@ def send_email(message):
152
149
153
150
app = Flask (__name__ )
154
151
app .config .from_object (Config )
155
- recaptcha = ReCaptcha (app )
156
152
157
153
# Initialize rate limiting
158
154
limiter = Limiter (get_remote_address , app = app , default_limits = ["200 per day" , "50 per hour" ])
159
155
160
156
# Configure logging
161
157
log_file = os .environ .get ('LOG_FILE' , '' )
162
-
163
158
if log_file :
164
159
logging .basicConfig (filename = log_file , level = logging .INFO )
165
160
else :
166
161
logging .basicConfig (level = logging .INFO )
167
162
168
163
@app .route ('/' , methods = ['GET' ])
169
164
def index ():
170
- return render_template ('index.html' , notice = '' , hascaptcha = not Config .DEBUG_MODE , attachments_number = Config .NUMBER_OF_ATTACHMENTS , recaptcha_sitekey = RECAPTCHASITEKEY )
171
-
165
+ return render_template ('index.html' , notice = '' , hascaptcha = True , attachments_number = Config .NUMBER_OF_ATTACHMENTS , recaptcha_sitekey = RECAPTCHASITEKEY )
172
166
173
167
@app .route ('/submit-encrypted-data' , methods = ['POST' ])
174
- @limiter .limit ("5 per minute" )
168
+ @limiter .limit ("3 per minute" )
175
169
def submit ():
176
170
try :
177
171
# Parse JSON data from request
178
172
data = request .get_json ()
179
173
180
- # Validate ReCaptcha unless in debug mode
181
- if not Config .DEBUG_MODE :
182
- recaptcha_response = data .get ('g-recaptcha-response' , '' )
183
- try :
184
- validate_recaptcha (recaptcha_response )
185
- except ValueError as e :
186
- return jsonify ({'status' : 'failure' , 'message' : str (e )}), 400
174
+ # Validate ReCaptcha
175
+ recaptcha_response = data .get ('g-recaptcha-response' , '' )
176
+ if not recaptcha_response :
177
+ logging .warning (f"Missing ReCaptcha response. Potential bypass attempt detected from IP: { request .remote_addr } " )
178
+ return jsonify ({'status' : 'failure' , 'message' : 'Missing ReCaptcha token' }), 400
179
+
180
+ try :
181
+ validate_recaptcha (recaptcha_response )
182
+ except ValueError as e :
183
+ return jsonify ({'status' : 'failure' , 'message' : str (e )}), 400
187
184
188
- # Extract fields from JSON data
189
185
message = data ['message' ]
190
186
recipient = data ['recipient' ]
191
187
reference = data .get ('reference' , '' )
@@ -197,7 +193,6 @@ def submit():
197
193
if not valid_recipient (recipient ):
198
194
raise ValueError ('Error: Invalid recipient!' )
199
195
200
- # Get submission statistics
201
196
date = datetime .now ().strftime ('%Y-%m-%d %H:%M:%S' )
202
197
message_length = len (message )
203
198
file_count = len (files )
@@ -212,24 +207,27 @@ def submit():
212
207
213
208
message = create_email (to_email , identifier , message , files , reference )
214
209
215
- if Config .DEBUG_MODE :
216
- print (f"Attempt to send email to { to_email } " )
217
- print (message .get ())
218
- send_email (message )
219
- else :
220
- send_email (message )
210
+ send_email (message )
221
211
222
212
notice = f'Thank you! The relevant team was notified of your submission. You could use the following identifier to refer to it in correspondence: <b>{ identifier } </b>'
223
213
224
- # Return success response
225
214
return jsonify ({'status' : 'success' , 'message' : notice })
226
215
227
216
except Exception as e :
228
- # Log error message and return failure response
229
217
error_message = "An unexpected error occurred. Please try again later."
230
218
logging .error (f"Internal error: { str (e )} " )
231
219
return jsonify ({'status' : 'failure' , 'message' : error_message })
232
220
221
+ @app .errorhandler (429 )
222
+ def rate_limit_exceeded (e ):
223
+ """
224
+ Handles requests that exceed the rate limit.
225
+ """
226
+ return jsonify ({
227
+ 'status' : 'failure' ,
228
+ 'message' : 'Rate limit exceeded. You can only submit once per minute. Please try again later.'
229
+ }), 429
230
+
233
231
234
232
@app .errorhandler (413 )
235
233
def error413 (e ):
0 commit comments