Skip to content

Commit 5eaac4a

Browse files
Merge pull request #987 from aniket866/mine
backend/app.py: File upload endpoint missing MIME type and extension validation
2 parents 82441f7 + dc9cceb commit 5eaac4a

3 files changed

Lines changed: 93 additions & 0 deletions

File tree

‎backend/app.py‎

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@
2020
import requests
2121
import secrets
2222
from urllib.parse import urlencode
23+
from werkzeug.utils import secure_filename
24+
import magic
2325

2426
import logging
2527
from datetime import datetime, timedelta, timezone
@@ -1904,6 +1906,75 @@ def logout():
19041906
return resp, status
19051907

19061908

1909+
UPLOAD_FOLDER = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'uploads', 'avatars')
1910+
os.makedirs(UPLOAD_FOLDER, exist_ok=True)
1911+
app.config['UPLOAD_FOLDER'] = UPLOAD_FOLDER
1912+
ALLOWED_EXTENSIONS = {'png', 'jpg', 'jpeg', 'gif'}
1913+
ALLOWED_MIME_TYPES = {'image/png', 'image/jpeg', 'image/gif'}
1914+
1915+
def allowed_file(filename):
1916+
return '.' in filename and filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS
1917+
1918+
@app.route('/api/v1/users/<int:user_id>/avatar', methods=['POST'])
1919+
@jwt_required()
1920+
def upload_avatar(user_id):
1921+
"""Upload and update user avatar with MIME type and magic number validation."""
1922+
current_user_id = get_jwt_identity()
1923+
1924+
if str(user_id) != str(current_user_id):
1925+
return forbidden_error("Unauthorized access to another user's profile")
1926+
1927+
if 'file' not in request.files:
1928+
return jsonify({"error": "No file part in the request"}), 400
1929+
1930+
file = request.files['file']
1931+
1932+
if file.filename == '':
1933+
return jsonify({"error": "No file selected for uploading"}), 400
1934+
1935+
if not allowed_file(file.filename):
1936+
return jsonify({"error": "Allowed file types are png, jpg, jpeg, gif"}), 400
1937+
1938+
# Validate Content-Type header
1939+
file_content_type = file.content_type
1940+
if file_content_type not in ALLOWED_MIME_TYPES:
1941+
return jsonify({"error": "Invalid Content-Type"}), 400
1942+
1943+
# Verify file magic numbers
1944+
file_bytes = file.read(2048)
1945+
file.seek(0) # Reset file pointer after reading
1946+
1947+
try:
1948+
mime = magic.Magic(mime=True)
1949+
file_mime = mime.from_buffer(file_bytes)
1950+
1951+
if file_mime not in ALLOWED_MIME_TYPES:
1952+
return jsonify({"error": "Invalid file content (magic number mismatch)"}), 400
1953+
except Exception as e:
1954+
logger.error(f"Error validating file magic numbers: {e}")
1955+
return jsonify({"error": "Could not validate file format"}), 500
1956+
1957+
filename = secure_filename(file.filename)
1958+
unique_filename = f"user_{user_id}_{filename}"
1959+
filepath = os.path.join(app.config['UPLOAD_FOLDER'], unique_filename)
1960+
1961+
try:
1962+
file.save(filepath)
1963+
1964+
user = User.query.get(user_id)
1965+
if user:
1966+
user.profile_picture = f"/uploads/avatars/{unique_filename}"
1967+
db.session.commit()
1968+
1969+
return jsonify({
1970+
"message": "Avatar uploaded successfully",
1971+
"profile_picture": f"/uploads/avatars/{unique_filename}"
1972+
}), 200
1973+
except Exception as e:
1974+
logger.error(f"Failed to save avatar: {e}")
1975+
return jsonify({"error": "Failed to save file"}), 500
1976+
1977+
19071978
@app.route('/api/v1/auth/forgot-password', methods=['POST'])
19081979
@csrf.exempt
19091980
@limiter.limit("5 per minute")

‎backend/requirements.txt‎

28 Bytes
Binary file not shown.

‎frontend/data/contributors.json‎

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -197,6 +197,28 @@
197197
"site_admin": false,
198198
"contributions": 23
199199
},
200+
{
201+
"login": "github-actions[bot]",
202+
"id": 41898282,
203+
"node_id": "MDM6Qm90NDE4OTgyODI=",
204+
"avatar_url": "https://avatars.githubusercontent.com/in/15368?v=4",
205+
"gravatar_id": "",
206+
"url": "https://api.github.com/users/github-actions%5Bbot%5D",
207+
"html_url": "https://github.com/apps/github-actions",
208+
"followers_url": "https://api.github.com/users/github-actions%5Bbot%5D/followers",
209+
"following_url": "https://api.github.com/users/github-actions%5Bbot%5D/following{/other_user}",
210+
"gists_url": "https://api.github.com/users/github-actions%5Bbot%5D/gists{/gist_id}",
211+
"starred_url": "https://api.github.com/users/github-actions%5Bbot%5D/starred{/owner}{/repo}",
212+
"subscriptions_url": "https://api.github.com/users/github-actions%5Bbot%5D/subscriptions",
213+
"organizations_url": "https://api.github.com/users/github-actions%5Bbot%5D/orgs",
214+
"repos_url": "https://api.github.com/users/github-actions%5Bbot%5D/repos",
215+
"events_url": "https://api.github.com/users/github-actions%5Bbot%5D/events{/privacy}",
216+
"received_events_url": "https://api.github.com/users/github-actions%5Bbot%5D/received_events",
217+
"type": "Bot",
218+
"user_view_type": "public",
219+
"site_admin": false,
220+
"contributions": 13
221+
},
200222
{
201223
"login": "github-actions[bot]",
202224
"id": 41898282,

0 commit comments

Comments
 (0)