Skip to content

Complete security audit of Flextensioons, identifying key vulnerabilities and areas for improvement.#413

Open
cycomachead wants to merge 4 commits into
mainfrom
cycomachead/ai/113/1
Open

Complete security audit of Flextensioons, identifying key vulnerabilities and areas for improvement.#413
cycomachead wants to merge 4 commits into
mainfrom
cycomachead/ai/113/1

Conversation

@cycomachead
Copy link
Copy Markdown
Contributor

Security Audit Remediation: Critical & High Vulnerability Fixes

Resolves four security vulnerabilities identified in the Flextensions security audit, covering privilege escalation, credential exposure, account takeover, and mass-assignment/IDOR issues.

  • Breaking?

Changes

C1 — Authorization bypass in AssignmentsController (privilege escalation + IDOR)
The toggle_enabled action previously accepted params[:role] from the client, allowing any authenticated user to pass role=instructor and toggle assignments in courses they don't belong to. The role is now derived exclusively server-side via course.user_role(@user). The Stimulus controller no longer sends role or user_id in the request body, and the view no longer embeds those values in data-* attributes.

C3 — Hard-coded Gradescope credentials in .env.example
Replaced a real-looking service account email and password with obvious placeholder values and added a comment explicitly warning against committing live credentials.

C4 — Account-linking logic enabling unauthorized account takeover
Split find_or_create_user into two distinct paths:

  • canvas_lookup_or_create: canonical production path — matches by canvas_uid, updates email on match, and refuses to silently re-key an existing account when a different canvas_uid presents a matching email (returns nil, surfacing an error to the user).
  • developer_lookup_or_create: intentional dev/test masquerade path — preserves the legacy email-match behavior so developers can log in as existing users without Canvas. Gated by developer_login_allowed? in the callback and the existing env check in omniauth.rb, with comments explaining the contract.

H2 — Mass-assignment and IDOR in RequestsController

  • Removed :user_id from request_params permit list; ownership is now set explicitly by the controller (user: @user for students, user: student for the instructor create_for_student flow).
  • Added assignment_in_course? helper that scopes assignment lookups to @course's CourseToLms records, preventing cross-course assignment references in create, update, and create_for_student.
  • Added student_enrolled_in_course helper to verify the target student is actually enrolled in @course before an instructor can file on their behalf.

Testing

Added regression specs for each fix:

  • assignments_controller_spec.rb: Covers student-in-course rejection, instructor-in-different-course rejection, and unenrolled user rejection — all with and without a spoofed role=instructor param.
  • requests_controller_spec.rb: Covers cross-course assignment rejection on create and update, client-supplied user_id being ignored on create and update, and cross-course assignment rejection in create_for_student.
  • session_controller_spec.rb: Covers canvas-provider email-collision refusal, canvas-provider canvas_uid match, developer masquerade (intentional), and developer-provider rejection when Rails.env is production.

All 163 existing controller specs continue to pass.

Documentation

No external documentation required. Code comments in session_controller.rb, requests_controller.rb, and config/initializers/omniauth.rb explain the security contracts inline for future maintainers.

Checklist

  • Name of branch corresponds to story

Superconductor Ticket Implementation | App Preview | Guided Review

cycomachead and others added 4 commits May 8, 2026 13:56
…e_enabled

The role check trusted a client-supplied params[:role], which the
Stimulus controller passed through from a data-* attribute. Any
authenticated user could PATCH /assignments/:id/toggle_enabled with
role=instructor and toggle assignments in courses where they were a
student or had no enrollment at all.

Now the role is looked up exclusively via course.user_role(current_user),
the JS no longer sends role/user_id, and the view drops the data-role /
data-user-id attributes that fed the bypass. Adds spec coverage for
students, foreign-course instructors, and the no-enrollment case all
attempting role=instructor.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…ders

The previous .env.example shipped a plausible-looking Gradescope password
(KoH-z92-oVJ-yqr) and a real-looking service-account email. Even though
those values were not the live credential, anything that looks like a
secret in a sample file invites confusion (and gets indexed by secret
scanners as a leak). Replace both with obvious placeholders and add a
warning not to commit live values.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…per login

The find_or_create_user logic always matched first by email and silently
re-keyed the existing user's canvas_uid. That is intentional for the
developer omniauth strategy (whose whole point is "log in as an existing
user without Canvas"), but for the real Canvas provider it would let any
auth response carrying a victim's email rewrite that user's identity.

Split the two paths:
- Canvas (and any non-developer provider) now matches strictly by
  canvas_uid, updates email/name on that user, and refuses to merge two
  identities when the email is already taken by a different account.
- Developer keeps the email-first masquerade behavior, but only runs
  when Rails.env is development or test. The omniauth_callback action
  now hard-fails developer-provider responses in any other environment,
  backing up the existing initializer-level guard.

Adds spec coverage for: canvas-provider email collision (refused),
canvas-provider canvas_uid match (still updates email), developer
masquerade (still works), and developer-provider rejection in
production.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
request_params permitted :user_id, which on PATCH #update would let a
student reassign their request to another user (the create flow merged
user: @user so it was incidentally safe; update did not). The
assignment_id param was also never checked against @Course, so a
request could be filed against an assignment from a course the
submitter doesn't belong to.

- Drop :user_id from request_params. Both create paths already set the
  owner explicitly (`user: @user` for self-service, `user: student` in
  the instructor `create_for_student` flow), so this only removes the
  mass-assignment escape hatch.
- Add assignment_in_course? and reject create/update/create_for_student
  when the assignment_id resolves to a different course's CourseToLms.
- Add student_enrolled_in_course for create_for_student so an instructor
  cannot file requests on behalf of someone outside their course
  roster.

Adds spec coverage for the user_id mass-assignment attempt on both
create and update, and for the cross-course assignment_id attempt on
both. Also bumps a few hardcoded canvas_uids in pre-existing tests to
string-prefixed values so they do not collide with the user factory's
numeric sequence as more tests are added.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
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