feature: port routes/index.js to Flask blueprint (routes/__init__.py)#33
Conversation
Implement main_bp Blueprint with all handlers from the original Express routes/index.js, preserving intentional vulnerabilities (NoSQL injection, open redirect, command injection, zip slip, SSTI, prototype-pollution analogue) for the Snyk goof security-education app. Co-Authored-By: Eashan Sinha <eashan.sinha@cognition.ai>
🤖 Devin AI EngineerI'll be helping with this pull request! Here's what you should know: ✅ I will automatically:
Note: I can only respond to comments from users who have write access to this repository. ⚙️ Control Options:
|
| user = users.find_one({ | ||
| 'username': body.get('username'), | ||
| 'password': body.get('password'), | ||
| }) |
There was a problem hiding this comment.
🚩 Login handler drops the email validation present in the original JS
The original loginHandler in routes/index.js:38 calls validator.isEmail(req.body.username) before querying MongoDB, returning 401 immediately if the username isn't a valid email. The Python port at routes/__init__.py:91-94 omits this check entirely and passes body.get('username') directly to users.find_one(). While the NoSQL injection is marked as intentional, the removal of the email validation widens the attack surface beyond the original — the JS version at least required a valid email string before querying, which blocked some injection payloads (e.g., {"$gt": ""} is not a valid email). This may be intentional for the demo, but it's a behavioral divergence worth noting.
Was this helpful? React with 👍 or 👎 to provide feedback.
There was a problem hiding this comment.
This divergence is intentional and required by the migration spec for this PR. The task explicitly calls for POST /login to read the JSON body and pass username/password directly into users.find_one({...}) without sanitizing, so the NoSQL injection works. As you noted, the original validator.isEmail() check would block payloads like {"$gt": ""} and effectively neuter the intended vulnerability. This is a deliberately-vulnerable Snyk "goof" demo, so I'm intentionally not re-adding the email validation. Leaving the code as-is.
| profile = request.get_json(force=True, silent=True) or request.form.to_dict() | ||
| return render_template('account.html', **profile) |
There was a problem hiding this comment.
🚩 save_account_details removes all input validation present in original
The original Express handler at routes/index.js:89-113 validates the profile with validator.isEmail(), validator.isMobilePhone(), validator.isAscii() before rendering. The Python port at line 128-129 does no validation at all and passes all user-supplied keys directly as template kwargs via **profile. The comment says this is "loose handling," but this is a meaningful behavioral difference: the original would reject invalid input and render a blank form, while the Python version always renders with user data. This also means any key in the JSON/form body becomes a Jinja2 template variable, which could shadow Flask context variables (e.g., request, session, config).
Was this helpful? React with 👍 or 👎 to provide feedback.
Debug
…rint - create(): return 400 for missing content instead of silently inserting a 'None' todo (str(None)). - chat: guard _state['last_id'] read/increment, message append, and message-list rebuild with a threading.Lock to avoid duplicate ids under threaded Flask. Co-Authored-By: Eashan Sinha <eashan.sinha@cognition.ai>
Python re.match only anchors at the start; the original item.match(imgRegex) searches anywhere. Using re.search restores detection of image markdown (and the intentional command injection) when content has leading text. Co-Authored-By: Eashan Sinha <eashan.sinha@cognition.ai>
- add _json_body() helper returning {} for non-object bodies and route all
endpoints through it (login/account/create/update/chat), so a malformed
body yields a normal response instead of a 500 AttributeError.
- guard find_user against non-dict auth.
- only deep_merge dict chat messages (mirrors lodash.merge ignoring non-object
sources); dict messages still flow through unfiltered, preserving the
prototype-pollution analogue.
Co-Authored-By: Eashan Sinha <eashan.sinha@cognition.ai>
- create() now sends the plain todo content as the 302 body instead of
base64-encoding it. The original JS uses todo.content.toString('base64'),
which is String.prototype.toString (ignores the 'base64' arg) and returns
the string unchanged; only Buffer.prototype.toString encodes. Dropped the
now-unused base64 import.
- chat_get snapshots messages under _chat_lock before serializing, matching
the locking used by chat_add/chat_delete and avoiding a data race.
Co-Authored-By: Eashan Sinha <eashan.sinha@cognition.ai>
| def save_account_details(): | ||
| # Loose handling that mirrors the original save_account_details: the | ||
| # submitted profile is rendered back as-is without strict validation. | ||
| profile = _json_body() or request.form.to_dict() | ||
| return render_template('account.html', **profile) |
There was a problem hiding this comment.
🚩 save_account_details removes all input validation present in the original
The original save_account_details in routes/index.js:89-113 validates the submitted profile fields (email format, phone format, ASCII checks on name/country) before rendering. The Python port at line 139 simply does profile = _json_body() or request.form.to_dict() and renders with no validation. The file's header comment (line 6) says intentional vulnerabilities are preserved, but this goes further — it removes validation that the original did have, creating a new attack surface (arbitrary template variables via **profile at line 140 could potentially conflict with Jinja2 internals). This may be intentional for the demo, but it's worth noting as a deliberate weakening beyond the original.
Was this helpful? React with 👍 or 👎 to provide feedback.
There was a problem hiding this comment.
This one is intentional per the migration spec, which calls for "basic (loose) handling" of POST /account_details with no validation — consistent with this being a deliberately-vulnerable demo app where we don't add sanitization/validation. The original's validator.isEmail/isMobilePhone/isAscii gate is dropped on purpose (same rationale as the dropped isEmail gate on login). Note the keys come from JSON object keys or request.form keys, which are always strings, so **profile won't raise on non-string keys. I'm leaving the loose behavior as-is to match the spec; flagging here for visibility rather than re-adding validation.
- login_handler now reads JSON first then falls back to request.form, so the HTML login form (application/x-www-form-urlencoded) works again. app.js registers both bodyParser.json() and urlencoded(), so the original req.body accepted either; the JSON-first order preserves the NoSQL-injection path. - import: drop the CWD backup.txt read. The original JS callback does 'data = data' (a no-op via param shadowing) and fs.readFile is async, so data always stays 'No backup.txt file found'. Reading CWD backup.txt diverged. Co-Authored-By: Eashan Sinha <eashan.sinha@cognition.ai>
Summary
Ports the original Express
routes/index.js(367 lines) to a Flask Blueprint inroutes/__init__.pyas part of the Node.js→Python/Flask migration. This is the deliberately-vulnerable Snyk "goof" demo app, so all intentional vulnerabilities are preserved (no sanitization/validation/escaping added).Exposes the integration contract the parent wires up:
All 16 routes register cleanly (verified by registering the blueprint on a Flask app with a stubbed
models.mongo):GET/POST /login,GET /,GET /admin,GET/POST /account_details,GET /logout,POST /create,GET /destroy/<id>,GET /edit/<id>,POST /update/<id>,POST /import,GET /about_new,GET/PUT/DELETE /chat.Preserved vulnerabilities (faithful to original behavior):
POST /loginpasses the raw JSON body straight intousers.find_one({'username':..., 'password':...}).redirect(redirect_page)taken from request body with no validation.POST /create, when content matches the image-markdown regex, runsos.system('identify ' + url).POST /importcallszipfile.ZipFile(...).extractall('/tmp/extracted_files')with no path validation.GET /about_newbuilds a template string with the unsanitizeddevicequery param and renders it viarender_template_string(notrender_template).PUT /chatmerges the user-suppliedmessagevia a custom recursivedeep_mergethat does NOT filter__class__/__proto__/__init__keys. Hardcodedusers_chatlist (named to avoid colliding with the imported Mongouserscollection).Session protection mirrors the original
isLoggedInmiddleware via alogin_requireddecorator (session.get('loggedIn') == 1elseredirect('/')).Verification
python -c "import ast; ast.parse(open('routes/__init__.py').read())"→ OKpython -m pyflakes routes/__init__.py→ cleanmodels.mongo).?device={{7*7}}→49; NoSQLi login→302 /admin; open redirect→302 http://evil.com; chat__class__key merged through; DELETE withoutcanDelete→403.Templates (
index.html,edit.html,admin.html,account.html) are produced by a parallel session undertemplates/;about_newneeds no template file. Full runtime is verified by the parent at integration.Constraints honored
Only
routes/__init__.pyis committed. No changes torequirements.txt,app.py,models/,routes/users.py,templates/, ortests/.Link to Devin session: https://app.devin.ai/sessions/bae8bc70b3334453b3d6d1fcac6f216f
Requested by: @eashansinha
Devin Review
837c96c