CVE-2014-125028: Cross-Site Request Forgery (CSRF) (CWE-352)
value: A vulnerability was found in valtech IDP Test Client and classified as problematic. Affected by this issue is some unknown functionality of the file python-flask/main.py. The manipulation leads to cross-site request forgery. The attack may be launched remotely. The name of the patch is f1e7b3d431c8681ec46445557125890c14fa295f. It is recommended to apply a patch to fix this issue. The identifier of this vulnerability is VDB-217148.
CWE description: The web application does not, or can not, sufficiently verify whether a well-formed, valid, consistent request was intentionally provided by the user who submitted the request.
CVSS v3 base score: 8.8
Published: 2022-12-31T20:15Z
Repository: https://github.com/valtech/valtech-idp-test-clients
Language: Python
Files changed: python-flask/main.py
Commit date: 2014-09-25 15:55:45 +0200
Commit message: Add CSRF protection using state to python-flask

Diff: diff --git a/python-flask/main.py b/python-flask/main.py
index ff77483..35a9de5 100644
--- a/python-flask/main.py
+++ b/python-flask/main.py
@@ -1,9 +1,10 @@
-from flask import Flask, render_template, request, session, redirect
+from flask import Flask, render_template, request, session, redirect, make_response
 import os, sys
 import requests
 import jwt
+import uuid
 
 CLIENT_ID = 'valtech.idp.testclient.local'
 CLIENT_SECRET = os.environ.get('CLIENT_SECRET')
 
 if CLIENT_SECRET is None:
@@ -25,16 +26,31 @@ def index():
   return render_template('index.html', header=header, text=text)
 
 @app.route('/sign-in')
 def sign_in():
   if session.get('signed_in') != None: return redirect('/')
-  authorize_url = 'https://stage-id.valtech.com/oauth2/authorize?response_type=%s&client_id=%s&scope=%s' % ('code', CLIENT_ID, 'email openid')
-  return redirect(authorize_url)
+
+  # state is used for CSRF protection. the client generates a value and stores it
+  # for the user somewhere (in a cookie or in a session). it then passes the same value
+  # in the state parameter in the authorize request. IDP will mirror the state value
+  # to the redirect URI. the client should then make sure the state value it has stored
+  # matches what it receives in the callback
+  state = str(uuid.uuid4())
+
+  authorize_url = 'https://stage-id.valtech.com/oauth2/authorize?response_type=%s&client_id=%s&scope=%s&state=%s' % ('code', CLIENT_ID, 'email openid', state)
+
+  resp = make_response(redirect(authorize_url))
+  resp.set_cookie('python-flask-csrf', state)
+  return resp
 
 @app.route('/sign-in/callback')
 def sign_in_callback():
   code = request.args.get('code')
+  state = request.args.get('state')
+
+  if state != request.cookies.get('python-flask-csrf'):
+    raise Exception("Possible CSRF detected (state does not match stored state)")
 
   # as both scope openid and email was requested on authorize request above, the client
   # will receive both an access_token (according to OAuth 2) AND an id_token (according to OpenID Connect)
   tokens = exchange_code_for_tokens(code)
 
@@ -47,12 +63,14 @@ def sign_in_callback():
   # (but the id_token should not be passed around in plaintext where it can be modified by a man-in-the-middle)
   user_info = jwt.decode(tokens["id_token"], verify=False)
 
   session['signed_in'] = True
   session['email'] = user_info['email']
-  
-  return redirect('/')
+
+  resp = make_response(redirect('/'))
+  resp.set_cookie('python-flask-csrf', '', expires=0)
+  return resp
 
 @app.route('/sign-out')
 def sign_out():
   session.clear()
   return redirect('https://stage-id.valtech.com/oidc/end-session?client_id=%s' % CLIENT_ID)

Vulnerable code: from flask import Flask, render_template, request, session, redirect
  authorize_url = 'https://stage-id.valtech.com/oauth2/authorize?response_type=%s&client_id=%s&scope=%s' % ('code', CLIENT_ID, 'email openid')
  return redirect(authorize_url)
  
  return redirect('/')
Fixed code: from flask import Flask, render_template, request, session, redirect, make_response
import uuid

  # state is used for CSRF protection. the client generates a value and stores it
  # for the user somewhere (in a cookie or in a session). it then passes the same value
  # in the state parameter in the authorize request. IDP will mirror the state value
  # to the redirect URI. the client should then make sure the state value it has stored
  # matches what it receives in the callback
  state = str(uuid.uuid4())

  authorize_url = 'https://stage-id.valtech.com/oauth2/authorize?response_type=%s&client_id=%s&scope=%s&state=%s' % ('code', CLIENT_ID, 'email openid', state)

  resp = make_response(redirect(authorize_url))
  resp.set_cookie('python-flask-csrf', state)
  return resp
  state = request.args.get('state')

  if state != request.cookies.get('python-flask-csrf'):
    raise Exception("Possible CSRF detected (state does not match stored state)")

  resp = make_response(redirect('/'))
  resp.set_cookie('python-flask-csrf', '', expires=0)
  return resp