Skip to content

Commit a5e7aeb

Browse files
ZinkelburgerStykMartin
authored andcommitted
fix: add authorization checks to XML-RPC stop() endpoints
The XML-RPC methods jobs.stop(), recipesets.stop(), recipes.stop(), and recipetasks.stop() only required authentication (not_anonymous) but did not verify that the caller owns the job or has the stop_task permission. This allowed any authenticated user to cancel any other user's job by calling these endpoints directly, bypassing the can_stop() check enforced by taskactions.stop(). Add can_stop(identity.current.user) checks to all four endpoints, matching the authorization already present in taskactions.stop() and the REST API (POST /jobs/<id>/status). Lab controllers are unaffected because they authenticate with the stop_task permission, which can_stop() already permits. Made-with: Cursor
1 parent 8627129 commit a5e7aeb

5 files changed

Lines changed: 120 additions & 0 deletions

File tree

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
2+
# This program is free software; you can redistribute it and/or modify
3+
# it under the terms of the GNU General Public License as published by
4+
# the Free Software Foundation; either version 2 of the License, or
5+
# (at your option) any later version.
6+
7+
"""
8+
Regression tests for missing authorization checks on the XML-RPC
9+
jobs.stop(), recipesets.stop(), recipes.stop(), and recipetasks.stop()
10+
endpoints.
11+
"""
12+
13+
import xmlrpclib
14+
from turbogears.database import session
15+
from bkr.inttest.server.selenium import XmlRpcTestCase
16+
from bkr.inttest import data_setup
17+
from bkr.server.model import TaskStatus
18+
19+
20+
class UnauthorizedJobCancelViaJobsStopTest(XmlRpcTestCase):
21+
22+
def setUp(self):
23+
with session.begin():
24+
self.victim = data_setup.create_user(password=u'victim')
25+
self.attacker = data_setup.create_user(password=u'attacker')
26+
self.job = data_setup.create_running_job(owner=self.victim)
27+
self.job_id = self.job.id
28+
self.server = self.get_server()
29+
self.server.auth.login_password(self.attacker.user_name, 'attacker')
30+
31+
def test_taskactions_stop_rejects_unauthorized_user(self):
32+
"""Baseline: the protected path correctly denies the attacker."""
33+
try:
34+
self.server.taskactions.stop(
35+
'J:%s' % self.job_id, 'cancel', 'authz test')
36+
self.fail('taskactions.stop should have denied the attacker')
37+
except xmlrpclib.Fault, e:
38+
self.assertIn("don't have permission", e.faultString)
39+
40+
def test_jobs_stop_rejects_unauthorized_user(self):
41+
try:
42+
self.server.jobs.stop(self.job_id, 'cancel', 'authz test')
43+
self.fail('jobs.stop should have denied the attacker')
44+
except xmlrpclib.Fault, e:
45+
self.assertIn("don't have permission", e.faultString)
46+
with session.begin():
47+
session.refresh(self.job)
48+
self.assertNotEqual(self.job.status, TaskStatus.cancelled,
49+
'Job was cancelled despite the attacker not owning it')
50+
51+
52+
class UnauthorizedCancelViaRecipeSetsStopTest(XmlRpcTestCase):
53+
54+
def setUp(self):
55+
with session.begin():
56+
self.victim = data_setup.create_user(password=u'victim')
57+
self.attacker = data_setup.create_user(password=u'attacker')
58+
self.job = data_setup.create_running_job(owner=self.victim)
59+
self.recipeset_id = self.job.recipesets[0].id
60+
self.server = self.get_server()
61+
self.server.auth.login_password(self.attacker.user_name, 'attacker')
62+
63+
def test_recipesets_stop_rejects_unauthorized_user(self):
64+
try:
65+
self.server.recipesets.stop(
66+
self.recipeset_id, 'cancel', 'authz test')
67+
self.fail('recipesets.stop should have denied the attacker')
68+
except xmlrpclib.Fault, e:
69+
self.assertIn("don't have permission", e.faultString)
70+
71+
72+
class UnauthorizedCancelViaRecipesStopTest(XmlRpcTestCase):
73+
74+
def setUp(self):
75+
with session.begin():
76+
self.victim = data_setup.create_user(password=u'victim')
77+
self.attacker = data_setup.create_user(password=u'attacker')
78+
self.job = data_setup.create_running_job(owner=self.victim)
79+
self.recipe_id = self.job.recipesets[0].recipes[0].id
80+
self.server = self.get_server()
81+
self.server.auth.login_password(self.attacker.user_name, 'attacker')
82+
83+
def test_recipes_stop_rejects_unauthorized_user(self):
84+
try:
85+
self.server.recipes.stop(
86+
self.recipe_id, 'cancel', 'authz test')
87+
self.fail('recipes.stop should have denied the attacker')
88+
except xmlrpclib.Fault, e:
89+
self.assertIn("don't have permission", e.faultString)
90+
91+
92+
class UnauthorizedCancelViaRecipeTasksStopTest(XmlRpcTestCase):
93+
94+
def setUp(self):
95+
with session.begin():
96+
self.victim = data_setup.create_user(password=u'victim')
97+
self.attacker = data_setup.create_user(password=u'attacker')
98+
self.job = data_setup.create_running_job(owner=self.victim)
99+
self.task_id = self.job.recipesets[0].recipes[0].tasks[0].id
100+
self.server = self.get_server()
101+
self.server.auth.login_password(self.attacker.user_name, 'attacker')
102+
103+
def test_recipetasks_stop_rejects_unauthorized_user(self):
104+
try:
105+
self.server.recipetasks.stop(
106+
self.task_id, 'cancel', 'authz test')
107+
self.fail('recipetasks.stop should have denied the attacker')
108+
except xmlrpclib.Fault, e:
109+
self.assertIn("don't have permission", e.faultString)

Server/bkr/server/jobs.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -849,6 +849,8 @@ def stop(self, job_id, stop_type, msg=None):
849849
job = Job.by_id(job_id)
850850
except InvalidRequestError:
851851
raise BX('Invalid job ID: %s' % job_id)
852+
if not job.can_stop(identity.current.user):
853+
raise BX("You don't have permission to stop job %s" % job_id)
852854
if stop_type not in job.stop_types:
853855
raise BX('Invalid stop_type: %s, must be one of %s' %
854856
(stop_type, job.stop_types))

Server/bkr/server/recipes.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -211,6 +211,9 @@ def stop(self, recipe_id, stop_type, msg=None):
211211
recipe = Recipe.by_id(recipe_id)
212212
except InvalidRequestError:
213213
raise BX('Invalid recipe ID: %s' % recipe_id)
214+
if not recipe.recipeset.can_stop(identity.current.user):
215+
raise BX("You don't have permission to stop recipe %s"
216+
% recipe_id)
214217
if stop_type not in recipe.stop_types:
215218
raise BX('Invalid stop_type: %s, must be one of %s' %
216219
(stop_type, recipe.stop_types))

Server/bkr/server/recipesets.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -253,6 +253,9 @@ def stop(self, recipeset_id, stop_type, msg=None):
253253
recipeset = RecipeSet.by_id(recipeset_id)
254254
except InvalidRequestError:
255255
raise BX('Invalid recipeset ID: %s' % recipeset_id)
256+
if not recipeset.can_stop(identity.current.user):
257+
raise BX("You don't have permission to stop recipeset %s"
258+
% recipeset_id)
256259
if stop_type not in recipeset.stop_types:
257260
raise BX('Invalid stop_type: %s, must be one of %s' %
258261
(stop_type, recipeset.stop_types))

Server/bkr/server/recipetasks.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -201,6 +201,9 @@ def stop(self, task_id, stop_type, msg=None):
201201
task = RecipeTask.by_id(task_id)
202202
except InvalidRequestError:
203203
raise BX('Invalid task ID: %s' % task_id)
204+
if not task.can_stop(identity.current.user):
205+
raise BX("You don't have permission to stop task %s"
206+
% task_id)
204207
if stop_type not in task.stop_types:
205208
raise BX('Invalid stop_type: %s, must be one of %s' %
206209
(stop_type, task.stop_types))

0 commit comments

Comments
 (0)