-
Notifications
You must be signed in to change notification settings - Fork 67
Fix IDOR vulnerability #1347
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Fix IDOR vulnerability #1347
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -1248,6 +1248,23 @@ defmodule Cadet.Assessments do | |
| |> Repo.one() | ||
| end | ||
|
|
||
| def get_assessment_in_course(assessment_id, course_id) | ||
| when is_ecto_id(assessment_id) and is_ecto_id(course_id) do | ||
| Assessment | ||
| |> where(id: ^assessment_id, course_id: ^course_id) | ||
| |> Repo.one() | ||
| end | ||
|
|
||
| def get_question_in_course(question_id, course_id) | ||
| when is_ecto_id(question_id) and is_ecto_id(course_id) do | ||
| Question | ||
| |> where(id: ^question_id) | ||
| |> join(:inner, [q], a in assoc(q, :assessment)) | ||
| |> where([_, a], a.course_id == ^course_id) | ||
| |> preload([_, a], assessment: a) | ||
| |> Repo.one() | ||
| end | ||
|
|
||
| def delete_question(id) when is_ecto_id(id) do | ||
| Logger.info("Deleting question #{id}") | ||
|
|
||
|
|
@@ -1405,6 +1422,16 @@ defmodule Cadet.Assessments do | |
| |> Repo.one() | ||
| end | ||
|
|
||
| def get_submission_in_course(submission_id, course_id) | ||
| when is_ecto_id(submission_id) and is_ecto_id(course_id) do | ||
| Submission | ||
| |> where(id: ^submission_id) | ||
| |> join(:inner, [s], a in assoc(s, :assessment)) | ||
| |> where([_, a], a.course_id == ^course_id) | ||
| |> preload([_, a], assessment: a) | ||
| |> Repo.one() | ||
| end | ||
|
|
||
| def finalise_submission(submission = %Submission{}) do | ||
| Logger.info( | ||
| "Finalizing submission #{submission.id} for assessment #{submission.assessment_id}" | ||
|
|
@@ -3093,6 +3120,21 @@ defmodule Cadet.Assessments do | |
| end | ||
| end | ||
|
|
||
| def get_answer_in_course(answer_id, course_id) | ||
| when is_ecto_id(answer_id) and is_ecto_id(course_id) do | ||
| answer_query = | ||
| Answer | ||
| |> where(id: ^answer_id) | ||
| |> join(:inner, [a], q in assoc(a, :question)) | ||
| |> join(:inner, [_, q], ast in assoc(q, :assessment)) | ||
| |> where([_, _, ast], ast.course_id == ^course_id) | ||
|
|
||
| case Repo.exists?(answer_query) do | ||
| true -> get_answer(answer_id) | ||
| false -> {:error, {:forbidden, "Forbidden"}} | ||
| end | ||
| end | ||
|
Comment on lines
+3123
to
+3136
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This function currently performs two separate database queries: one with You can combine these into a single, more efficient query that both verifies the scope and fetches the answer. This avoids the extra database roundtrip. Assuming |
||
|
|
||
| @spec get_answers_in_submission(integer() | String.t()) :: | ||
| {:ok, {[Answer.t()], Assessment.t()}} | ||
| | {:error, {:bad_request, String.t()}} | ||
|
|
@@ -3200,7 +3242,7 @@ defmodule Cadet.Assessments do | |
| def update_grading_info( | ||
| %{submission_id: submission_id, question_id: question_id}, | ||
| attrs, | ||
| cr = %CourseRegistration{id: grader_id} | ||
| cr = %CourseRegistration{id: grader_id, course_id: course_id} | ||
| ) | ||
| when is_ecto_id(submission_id) and is_ecto_id(question_id) do | ||
| attrs = Map.put(attrs, "grader_id", grader_id) | ||
|
|
@@ -3213,7 +3255,9 @@ defmodule Cadet.Assessments do | |
| answer_query = | ||
| answer_query | ||
| |> join(:inner, [a], s in assoc(a, :submission)) | ||
| |> preload([_, s], submission: s) | ||
| |> join(:inner, [_, s], asst in assoc(s, :assessment)) | ||
| |> where([_, _, asst], asst.course_id == ^course_id) | ||
| |> preload([_, s, asst], submission: {s, assessment: asst}) | ||
|
|
||
| answer = Repo.one(answer_query) | ||
|
|
||
|
|
@@ -3267,10 +3311,16 @@ defmodule Cadet.Assessments do | |
| {:ok, nil} | {:error, {:forbidden | :not_found, String.t()}} | ||
| def force_regrade_submission( | ||
| submission_id, | ||
| _requesting_user = %CourseRegistration{id: grader_id} | ||
| _requesting_user = %CourseRegistration{id: grader_id, course_id: course_id} | ||
| ) | ||
| when is_ecto_id(submission_id) do | ||
| with {:get, sub} when not is_nil(sub) <- {:get, Repo.get(Submission, submission_id)}, | ||
| submission_query = | ||
| Submission | ||
| |> where(id: ^submission_id) | ||
| |> join(:inner, [s], asst in assoc(s, :assessment)) | ||
| |> where([_, asst], asst.course_id == ^course_id) | ||
|
|
||
| with {:get, sub} when not is_nil(sub) <- {:get, Repo.one(submission_query)}, | ||
| {:status, true} <- {:status, sub.student_id == grader_id or sub.status == :submitted} do | ||
| GradingJob.force_grade_individual_submission(sub, true) | ||
| {:ok, nil} | ||
|
|
@@ -3296,12 +3346,15 @@ defmodule Cadet.Assessments do | |
| def force_regrade_answer( | ||
| submission_id, | ||
| question_id, | ||
| _requesting_user = %CourseRegistration{id: grader_id} | ||
| _requesting_user = %CourseRegistration{id: grader_id, course_id: course_id} | ||
| ) | ||
| when is_ecto_id(submission_id) and is_ecto_id(question_id) do | ||
| answer = | ||
| Answer | ||
| |> where(submission_id: ^submission_id, question_id: ^question_id) | ||
| |> join(:inner, [a], q in assoc(a, :question)) | ||
| |> join(:inner, [_, q], asst in assoc(q, :assessment)) | ||
| |> where([_, _, asst], asst.course_id == ^course_id) | ||
| |> preload([:question, :submission]) | ||
| |> Repo.one() | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -4,6 +4,32 @@ defmodule CadetWeb.AdminGradingController do | |
|
|
||
| alias Cadet.{Assessments, Courses} | ||
|
|
||
| plug( | ||
| CadetWeb.Plug.EnsureResourceScope, | ||
| [resource: :submission, param: "submissionid", assign: :scoped_submission] | ||
| when action in [ | ||
| :show, | ||
| :update, | ||
| :unsubmit, | ||
| :unpublish_grades, | ||
| :publish_grades, | ||
| :autograde_submission, | ||
| :autograde_answer | ||
| ] | ||
| ) | ||
|
|
||
| plug( | ||
| CadetWeb.Plug.EnsureResourceScope, | ||
| [resource: :question, param: "questionid", assign: :scoped_question] | ||
| when action in [:update, :autograde_answer] | ||
| ) | ||
|
Comment on lines
+7
to
+25
|
||
|
|
||
| plug( | ||
| CadetWeb.Plug.EnsureResourceScope, | ||
| [resource: :assessment, param: "assessmentid", assign: :scoped_assessment] | ||
| when action in [:publish_all_grades, :unpublish_all_grades] | ||
| ) | ||
|
Comment on lines
+7
to
+31
|
||
|
|
||
| @doc """ | ||
| # Query Parameters | ||
| - `pageSize`: Integer. The number of submissions to return. Default 10. | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -5,6 +5,12 @@ defmodule CadetWeb.AdminTeamsController do | |
|
|
||
| alias Cadet.Accounts.{Teams, Team} | ||
|
|
||
| plug( | ||
| CadetWeb.Plug.EnsureResourceScope, | ||
| [resource: :team, param: "teamid", assign: :scoped_team] | ||
| when action in [:update, :delete] | ||
| ) | ||
|
Comment on lines
+8
to
+12
|
||
|
|
||
| def index(conn, %{"course_id" => course_id}) do | ||
| teams = Teams.all_teams_for_course(course_id) | ||
|
|
||
|
|
@@ -47,14 +53,9 @@ defmodule CadetWeb.AdminTeamsController do | |
| end | ||
| end | ||
|
|
||
| def update(conn, %{ | ||
| "teamId" => teamId, | ||
| "assessmentId" => assessmentId, | ||
| "student_ids" => student_ids | ||
| }) do | ||
| def update(conn, %{"assessmentId" => assessmentId, "student_ids" => student_ids}) do | ||
| team = | ||
| Team | ||
| |> Repo.get!(teamId) | ||
| conn.assigns.scoped_team | ||
| |> Repo.preload(assessment: [:config], team_members: [student: [:user]]) | ||
|
|
||
| case Teams.update_team(team, assessmentId, student_ids) do | ||
|
|
@@ -70,8 +71,8 @@ defmodule CadetWeb.AdminTeamsController do | |
| end | ||
| end | ||
|
|
||
| def delete(conn, %{"teamId" => team_id}) do | ||
| team = Repo.get(Team, team_id) | ||
| def delete(conn, %{"teamid" => _team_id}) do | ||
| team = conn.assigns.scoped_team | ||
|
|
||
| if team do | ||
| case Teams.delete_team(team) do | ||
|
Comment on lines
+74
to
78
|
||
|
|
@@ -90,10 +91,6 @@ defmodule CadetWeb.AdminTeamsController do | |
| end | ||
| end | ||
|
|
||
| def delete(conn, %{"course_id" => course_id, "teamid" => team_id}) do | ||
| delete(conn, %{"teamId" => team_id}) | ||
| end | ||
|
|
||
| swagger_path :index do | ||
| get("/admin/teams") | ||
|
|
||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
get_answer_in_course/2currently issues two DB queries for the success case (Repo.exists?and thenget_answer/1). Since this is used in a request plug, it adds avoidable overhead on every call. Consider fetching the scoped answer directly in a single query (e.g., join + preload andRepo.one) and returningnil/{:error, :forbidden}when not found, instead ofexists?+ re-fetch.