fix(security): verifier les fichiers televerses et retirer un intent Discord
SEC-021 -- rien ne validait le contrat signe upload_signed_contract se contentait d un nom de fichier non vide. ALLOWED_SIGNED_EXTENSIONS etait declaree juste a cote et jamais lue. Le fichier atterrissait sur le disque sous un nom que download_signed_contract sert ensuite : ce qu un joueur televerse est ce qu un gerant ouvre. Le nom seul ne suffisait pas non plus cote upload_contract, qui verifiait `.pdf` en fin de chaine -- payload.pdf ne dit rien des octets. pdf_upload_error() couvre les deux routes : extension dans la liste, puis signature %PDF- en tete de flux. Le flux est rembobine, l appelant enregistre toujours le fichier entier. OPS-014 -- intent Discord privilegie inutile Le bot demandait GUILD_MEMBERS et ne s en servait pas : rien n enumere ni ne recherche de membre de serveur, les personnes sont jointes par le discord_user_id enregistre sur leur compte. Retire. message_content reste : on_raw_reaction_add lit le texte de la reponse d un coach pour consigner un motif de refus. CI-003 et CI-005 sont deja appliques (permissions: contents: read, checkout@v4, exclusions de deploiement). L epinglage par SHA des actions n est pas fait : ce sont des actions GitHub de premiere partie, et l epingler sans Dependabot echange une exposition contre une autre. 11 tests, dont deux verifient que signer un contrat marche toujours et qu un autre joueur ne peut pas le faire. Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
+42
-17
@@ -35,9 +35,43 @@ import requests
|
||||
ALLOWED_CONTRACT_EXTENSIONS = {'pdf'}
|
||||
ALLOWED_SIGNED_EXTENSIONS = {'pdf'}
|
||||
|
||||
#: Every PDF starts with this. Checking the name alone accepted a file
|
||||
#: called anything.pdf holding anything at all.
|
||||
PDF_SIGNATURE = b'%PDF-'
|
||||
|
||||
users_bp = Blueprint('users', __name__, url_prefix='/users')
|
||||
|
||||
|
||||
def pdf_upload_error(file, allowed_extensions):
|
||||
"""Why this upload is not an acceptable PDF, or None if it is.
|
||||
|
||||
upload_signed_contract checked nothing beyond a non-empty filename —
|
||||
ALLOWED_SIGNED_EXTENSIONS was declared and never read — so a player
|
||||
could put an arbitrary file on the server under a name the application
|
||||
later hands back for download (SEC-021).
|
||||
|
||||
Args:
|
||||
file: The uploaded FileStorage, or None.
|
||||
allowed_extensions: Extensions to accept, lowercase and without dot.
|
||||
|
||||
Returns:
|
||||
str | None: A message to flash, or None when the file is acceptable.
|
||||
"""
|
||||
if file is None or not file.filename:
|
||||
return _('No file selected.')
|
||||
|
||||
stem, dot, extension = file.filename.rpartition('.')
|
||||
if not (stem and dot) or extension.lower() not in allowed_extensions:
|
||||
return _('Only PDF files are allowed for contracts.')
|
||||
|
||||
head = file.stream.read(len(PDF_SIGNATURE))
|
||||
file.stream.seek(0)
|
||||
if head != PDF_SIGNATURE:
|
||||
return _('That file is not a PDF, whatever its name says.')
|
||||
|
||||
return None
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Form helpers shared by the schema-validated routes
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -660,16 +694,10 @@ def upload_contract():
|
||||
flash(_('You do not have permission to upload a contract for this player.'), 'danger')
|
||||
return redirect(url_for('users.upload_contract'))
|
||||
|
||||
if 'contract_file' not in request.files:
|
||||
flash(_('No file selected.'), 'danger')
|
||||
return redirect(url_for('users.upload_contract'))
|
||||
|
||||
file = request.files['contract_file']
|
||||
if file.filename == '':
|
||||
flash(_('No file selected.'), 'danger')
|
||||
return redirect(url_for('users.upload_contract'))
|
||||
if not file.filename.lower().endswith('.pdf'):
|
||||
flash(_('Only PDF files are allowed for contracts.'), 'danger')
|
||||
file = request.files.get('contract_file')
|
||||
error = pdf_upload_error(file, ALLOWED_CONTRACT_EXTENSIONS)
|
||||
if error:
|
||||
flash(error, 'danger')
|
||||
return redirect(url_for('users.upload_contract'))
|
||||
|
||||
upload_dir = os.path.join(os.getcwd(), 'documents', 'contrats signés')
|
||||
@@ -717,13 +745,10 @@ def upload_signed_contract(contract_id):
|
||||
flash(_('Only the player can upload their signed contract.'), 'danger')
|
||||
return redirect(url_for('users.list_contracts'))
|
||||
|
||||
if 'signed_file' not in request.files:
|
||||
flash(_('No file selected.'), 'danger')
|
||||
return redirect(url_for('users.list_contracts'))
|
||||
|
||||
file = request.files['signed_file']
|
||||
if file.filename == '':
|
||||
flash(_('No file selected.'), 'danger')
|
||||
file = request.files.get('signed_file')
|
||||
error = pdf_upload_error(file, ALLOWED_SIGNED_EXTENSIONS)
|
||||
if error:
|
||||
flash(error, 'danger')
|
||||
return redirect(url_for('users.list_contracts'))
|
||||
|
||||
signed_filename = f"signed_{contract.stored_filename}"
|
||||
|
||||
Reference in New Issue
Block a user