Skip to content

feature: port routes/index.js to Flask blueprint (routes/__init__.py)#33

Open
devin-ai-integration[bot] wants to merge 6 commits into
devin/1780985717-flask-migrationfrom
devin/1780986085-routes-core
Open

feature: port routes/index.js to Flask blueprint (routes/__init__.py)#33
devin-ai-integration[bot] wants to merge 6 commits into
devin/1780985717-flask-migrationfrom
devin/1780986085-routes-core

Conversation

@devin-ai-integration

@devin-ai-integration devin-ai-integration Bot commented Jun 9, 2026

Copy link
Copy Markdown

Summary

Ports the original Express routes/index.js (367 lines) to a Flask Blueprint in routes/__init__.py as 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:

main_bp = Blueprint('main', __name__)            # module-level, exact name
from models.mongo import todos, users            # raw pymongo collections (built in parallel)

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):

  • NoSQL injectionPOST /login passes the raw JSON body straight into users.find_one({'username':..., 'password':...}).
  • Open redirect — on login success, redirect(redirect_page) taken from request body with no validation.
  • Command injectionPOST /create, when content matches the image-markdown regex, runs os.system('identify ' + url).
  • Zip slipPOST /import calls zipfile.ZipFile(...).extractall('/tmp/extracted_files') with no path validation.
  • SSTIGET /about_new builds a template string with the unsanitized device query param and renders it via render_template_string (not render_template).
  • Prototype-pollution analoguePUT /chat merges the user-supplied message via a custom recursive deep_merge that does NOT filter __class__/__proto__/__init__ keys. Hardcoded users_chat list (named to avoid colliding with the imported Mongo users collection).

Session protection mirrors the original isLoggedIn middleware via a login_required decorator (session.get('loggedIn') == 1 else redirect('/')).

Verification

  • python -c "import ast; ast.parse(open('routes/__init__.py').read())" → OK
  • python -m pyflakes routes/__init__.py → clean
  • Blueprint registers on a Flask app with all 16 rules/methods (stubbed models.mongo).
  • Smoke test of the test client confirmed: SSTI ?device={{7*7}}49; NoSQLi login→302 /admin; open redirect→302 http://evil.com; chat __class__ key merged through; DELETE without canDelete403.

Templates (index.html, edit.html, admin.html, account.html) are produced by a parallel session under templates/; about_new needs no template file. Full runtime is verified by the parent at integration.

Constraints honored

Only routes/__init__.py is committed. No changes to requirements.txt, app.py, models/, routes/users.py, templates/, or tests/.

Link to Devin session: https://app.devin.ai/sessions/bae8bc70b3334453b3d6d1fcac6f216f
Requested by: @eashansinha


Devin Review

Status Commit
🟢 Reviewed 837c96c
Open in Devin Review (Staging)

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-integration

Copy link
Copy Markdown
Author

🤖 Devin AI Engineer

I'll be helping with this pull request! Here's what you should know:

✅ I will automatically:

  • Address comments on this PR. Add '(aside)' to your comment to have me ignore it.
  • Look at CI failures and help fix them

Note: I can only respond to comments from users who have write access to this repository.

⚙️ Control Options:

  • Disable automatic comment, CI, and merge conflict monitoring

@devin-ai-integration devin-ai-integration Bot left a comment

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Devin Review found 3 potential issues.

View 4 additional findings in Devin Review.

Open in Devin Review

Comment thread routes/__init__.py
Comment thread routes/__init__.py Outdated
Comment thread routes/__init__.py
Comment on lines +91 to +94
user = users.find_one({
'username': body.get('username'),
'password': body.get('password'),
})

@devin-ai-integration devin-ai-integration Bot Jun 9, 2026

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚩 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.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@staging-devin-ai-integration staging-devin-ai-integration Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Devin Review found 2 potential issues.

Open in Devin Review (Staging)
Debug

Playground

Comment thread routes/__init__.py Outdated
Comment thread routes/__init__.py Outdated
Comment on lines +128 to +129
profile = request.get_json(force=True, silent=True) or request.form.to_dict()
return render_template('account.html', **profile)

@staging-devin-ai-integration staging-devin-ai-integration Bot Jun 9, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚩 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).

Open in Devin Review (Staging)

Was this helpful? React with 👍 or 👎 to provide feedback.

Debug

Playground

…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>
devin-ai-integration[bot]

This comment was marked as resolved.

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>
devin-ai-integration[bot]

This comment was marked as resolved.

- 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>
devin-ai-integration[bot]

This comment was marked as resolved.

- 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>

@devin-ai-integration devin-ai-integration Bot left a comment

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Devin Review found 3 new potential issues.

View 6 additional findings in Devin Review.

Open in Devin Review

Comment thread routes/__init__.py Outdated
Comment thread routes/__init__.py Outdated
Comment thread routes/__init__.py
Comment on lines +136 to +140
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)

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚩 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.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant