David Ko commited on
Commit
b4662d6
ยท
1 Parent(s): ac9962b

Security/session hardening: absolute 2-min expiry (SESSION_REFRESH_EACH_REQUEST=False), disable remember, anti-autofill login form, no-store cache on protected routes, clear session on logout

Browse files
Files changed (1) hide show
  1. api.py +44 -10
api.py CHANGED
@@ -48,7 +48,8 @@ CORS(app) # Enable CORS for all routes
48
  # ์‹œํฌ๋ฆฟ ํ‚ค ์„ค์ • (์„ธ์…˜ ์•”ํ˜ธํ™”์— ์‚ฌ์šฉ)
49
  app.config['SECRET_KEY'] = os.environ.get('SECRET_KEY', 'vision_llm_agent_secret_key')
50
  app.config['SESSION_TYPE'] = 'filesystem'
51
- app.config['PERMANENT_SESSION_LIFETIME'] = 120 # ์„ธ์…˜ ์œ ํšจ ์‹œ๊ฐ„ (์ดˆ)
 
52
 
53
  # Flask-Login ์„ค์ •
54
  login_manager = LoginManager()
@@ -1139,14 +1140,17 @@ LOGIN_TEMPLATE = '''
1139
  <body>
1140
  <div class="login-container">
1141
  <h1>Vision LLM Agent</h1>
1142
- <form action="/login" method="post">
 
 
 
1143
  <div class="form-group">
1144
  <label for="username">Username</label>
1145
- <input type="text" id="username" name="username" required>
1146
  </div>
1147
  <div class="form-group">
1148
  <label for="password">Password</label>
1149
- <input type="password" id="password" name="password" required>
1150
  </div>
1151
  <button type="submit">Login</button>
1152
  {% if error %}
@@ -1199,6 +1203,11 @@ def login():
1199
  @app.route('/logout')
1200
  def logout():
1201
  logout_user()
 
 
 
 
 
1202
  return redirect(url_for('login'))
1203
 
1204
  # ์ •์  ํŒŒ์ผ ์„œ๋น™์„ ์œ„ํ•œ ๋ผ์šฐํŠธ (๋กœ๊ทธ์ธ ๋ถˆํ•„์š”)
@@ -1229,7 +1238,12 @@ def serve_index_html():
1229
  session['username'] = current_user.username
1230
  session.modified = True
1231
 
1232
- return send_from_directory(app.static_folder, 'index.html')
 
 
 
 
 
1233
 
1234
  # ๊ธฐ๋ณธ ๊ฒฝ๋กœ ๋ฐ ๊ธฐํƒ€ ๊ฒฝ๋กœ ์ฒ˜๋ฆฌ (๋กœ๊ทธ์ธ ํ•„์š”)
1235
  @app.route('/', defaults={'path': ''}, methods=['GET'])
@@ -1240,28 +1254,48 @@ def serve_react(path):
1240
  print(f"Serving React frontend for path: {path}, user: {current_user.username if current_user.is_authenticated else 'not authenticated'}")
1241
  # ์ •์  ํŒŒ์ผ ์ฒ˜๋ฆฌ๋Š” ์ด์ œ ๋ณ„๋„ ๋ผ์šฐํŠธ์—์„œ ์ฒ˜๋ฆฌ
1242
  if path != "" and os.path.exists(os.path.join(app.static_folder, path)):
1243
- return send_from_directory(app.static_folder, path)
 
 
 
 
1244
  else:
1245
  # React ์•ฑ์˜ index.html ์„œ๋น™
1246
- return send_from_directory(app.static_folder, 'index.html')
 
 
 
 
1247
 
1248
  @app.route('/similar-images', methods=['GET'])
1249
  @login_required
1250
  def similar_images_page():
1251
  """Serve similar images search page"""
1252
- return send_from_directory(app.static_folder, 'similar-images.html')
 
 
 
 
1253
 
1254
  @app.route('/object-detection-search', methods=['GET'])
1255
  @login_required
1256
  def object_detection_search_page():
1257
  """Serve object detection search page"""
1258
- return send_from_directory(app.static_folder, 'object-detection-search.html')
 
 
 
 
1259
 
1260
  @app.route('/model-vector-db', methods=['GET'])
1261
  @login_required
1262
  def model_vector_db_page():
1263
  """Serve model vector DB UI page"""
1264
- return send_from_directory(app.static_folder, 'model-vector-db.html')
 
 
 
 
1265
 
1266
  @app.route('/api/status', methods=['GET'])
1267
  @login_required
 
48
  # ์‹œํฌ๋ฆฟ ํ‚ค ์„ค์ • (์„ธ์…˜ ์•”ํ˜ธํ™”์— ์‚ฌ์šฉ)
49
  app.config['SECRET_KEY'] = os.environ.get('SECRET_KEY', 'vision_llm_agent_secret_key')
50
  app.config['SESSION_TYPE'] = 'filesystem'
51
+ app.config['PERMANENT_SESSION_LIFETIME'] = timedelta(seconds=120) # ์„ธ์…˜ ์œ ํšจ ์‹œ๊ฐ„ (2๋ถ„)
52
+ app.config['SESSION_REFRESH_EACH_REQUEST'] = False # ์ ˆ๋Œ€ ๋งŒ๋ฃŒ(๋กœ๊ทธ์ธ ๊ธฐ์ค€ 2๋ถ„ ํ›„ ๋งŒ๋ฃŒ)
53
 
54
  # Flask-Login ์„ค์ •
55
  login_manager = LoginManager()
 
1140
  <body>
1141
  <div class="login-container">
1142
  <h1>Vision LLM Agent</h1>
1143
+ <form action="/login" method="post" autocomplete="off">
1144
+ <!-- hidden dummy fields to discourage Chrome autofill -->
1145
+ <input type="text" name="fakeusernameremembered" style="display:none" tabindex="-1" autocomplete="off">
1146
+ <input type="password" name="fakepasswordremembered" style="display:none" tabindex="-1" autocomplete="off">
1147
  <div class="form-group">
1148
  <label for="username">Username</label>
1149
+ <input type="text" id="username" name="username" required autocomplete="username" autocapitalize="none" autocorrect="off" spellcheck="false">
1150
  </div>
1151
  <div class="form-group">
1152
  <label for="password">Password</label>
1153
+ <input type="password" id="password" name="password" required autocomplete="current-password" autocapitalize="none" autocorrect="off" spellcheck="false">
1154
  </div>
1155
  <button type="submit">Login</button>
1156
  {% if error %}
 
1203
  @app.route('/logout')
1204
  def logout():
1205
  logout_user()
1206
+ # Clear server-side session fully
1207
+ try:
1208
+ session.clear()
1209
+ except Exception as e:
1210
+ print(f"[DEBUG] Error clearing session on logout: {e}")
1211
  return redirect(url_for('login'))
1212
 
1213
  # ์ •์  ํŒŒ์ผ ์„œ๋น™์„ ์œ„ํ•œ ๋ผ์šฐํŠธ (๋กœ๊ทธ์ธ ๋ถˆํ•„์š”)
 
1238
  session['username'] = current_user.username
1239
  session.modified = True
1240
 
1241
+ resp = send_from_directory(app.static_folder, 'index.html')
1242
+ # Prevent sensitive pages from being cached
1243
+ resp.headers['Cache-Control'] = 'no-store, no-cache, must-revalidate, max-age=0'
1244
+ resp.headers['Pragma'] = 'no-cache'
1245
+ resp.headers['Expires'] = '0'
1246
+ return resp
1247
 
1248
  # ๊ธฐ๋ณธ ๊ฒฝ๋กœ ๋ฐ ๊ธฐํƒ€ ๊ฒฝ๋กœ ์ฒ˜๋ฆฌ (๋กœ๊ทธ์ธ ํ•„์š”)
1249
  @app.route('/', defaults={'path': ''}, methods=['GET'])
 
1254
  print(f"Serving React frontend for path: {path}, user: {current_user.username if current_user.is_authenticated else 'not authenticated'}")
1255
  # ์ •์  ํŒŒ์ผ ์ฒ˜๋ฆฌ๋Š” ์ด์ œ ๋ณ„๋„ ๋ผ์šฐํŠธ์—์„œ ์ฒ˜๋ฆฌ
1256
  if path != "" and os.path.exists(os.path.join(app.static_folder, path)):
1257
+ resp = send_from_directory(app.static_folder, path)
1258
+ resp.headers['Cache-Control'] = 'no-store, no-cache, must-revalidate, max-age=0'
1259
+ resp.headers['Pragma'] = 'no-cache'
1260
+ resp.headers['Expires'] = '0'
1261
+ return resp
1262
  else:
1263
  # React ์•ฑ์˜ index.html ์„œ๋น™
1264
+ resp = send_from_directory(app.static_folder, 'index.html')
1265
+ resp.headers['Cache-Control'] = 'no-store, no-cache, must-revalidate, max-age=0'
1266
+ resp.headers['Pragma'] = 'no-cache'
1267
+ resp.headers['Expires'] = '0'
1268
+ return resp
1269
 
1270
  @app.route('/similar-images', methods=['GET'])
1271
  @login_required
1272
  def similar_images_page():
1273
  """Serve similar images search page"""
1274
+ resp = send_from_directory(app.static_folder, 'similar-images.html')
1275
+ resp.headers['Cache-Control'] = 'no-store, no-cache, must-revalidate, max-age=0'
1276
+ resp.headers['Pragma'] = 'no-cache'
1277
+ resp.headers['Expires'] = '0'
1278
+ return resp
1279
 
1280
  @app.route('/object-detection-search', methods=['GET'])
1281
  @login_required
1282
  def object_detection_search_page():
1283
  """Serve object detection search page"""
1284
+ resp = send_from_directory(app.static_folder, 'object-detection-search.html')
1285
+ resp.headers['Cache-Control'] = 'no-store, no-cache, must-revalidate, max-age=0'
1286
+ resp.headers['Pragma'] = 'no-cache'
1287
+ resp.headers['Expires'] = '0'
1288
+ return resp
1289
 
1290
  @app.route('/model-vector-db', methods=['GET'])
1291
  @login_required
1292
  def model_vector_db_page():
1293
  """Serve model vector DB UI page"""
1294
+ resp = send_from_directory(app.static_folder, 'model-vector-db.html')
1295
+ resp.headers['Cache-Control'] = 'no-store, no-cache, must-revalidate, max-age=0'
1296
+ resp.headers['Pragma'] = 'no-cache'
1297
+ resp.headers['Expires'] = '0'
1298
+ return resp
1299
 
1300
  @app.route('/api/status', methods=['GET'])
1301
  @login_required