CVE-2011-4952
Description
cobbler: Web interface lacks CSRF protection when using Django framework
AI Insight
LLM-synthesized narrative grounded in this CVE's description and references.
Cobbler's web interface lacked CSRF protection, allowing attackers to perform unauthorized actions on behalf of authenticated users.
Vulnerability
Description Cobbler, a Linux installation server, had a missing Cross-Site Request Forgery (CSRF) protection in its Django-based web interface. The commit [2] shows the addition of the csrf_protect decorator and RequestContext to enable Django's built-in CSRF protection, confirming the previous absence of such safeguards.
Exploitation
An attacker could craft a malicious request that, when visited by an authenticated Cobbler web interface user, would execute arbitrary actions on the user's behalf without their consent. The attack requires social engineering to trick the user into clicking a crafted link or visiting a malicious page while logged into the interface.
Impact
Successful exploitation allows unauthorized modification of Cobbler configurations, including adding or altering systems, profiles, or distributions. This can lead to further compromise of managed systems or service disruption. The vulnerability was reported in 2011 and assigned CVE-2011-4952 [1][3][4].
Mitigation
The fix was implemented in a commit [2] that enables Django's CSRF middleware. Users should upgrade to a patched version of Cobbler. No workarounds are mentioned in the references.
AI Insight generated on May 21, 2026. Synthesized from this CVE's description and the cited reference URLs; citations are validated against the source bundle.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
cobblerPyPI | < 2.6.0 | 2.6.0 |
Affected products
2- cobbler/cobblerv5Range: 2011-09-28
Patches
218eb1c06779bAdditional CSRF work. All URLs that modify state are now required to be POSTs only.
5 files changed · +78 −17
web/cobbler_web/templates/filter.tmpl+8 −2 modified@@ -1,5 +1,6 @@ {% if pageinfo %} <script type="text/javascript"> +// these functions depend on the "action" form defined in generic_list function add_filter() { field_name = document.getElementById("filter_field"); field_value = document.getElementById("filter_value"); @@ -8,9 +9,14 @@ function add_filter() { } else if (field_value.value == "") { alert("You must select a filter value."); } else { - location = '/cobbler_web/{{ what }}/modifylist/addfilter/'+field_name.value+':'+field_value.value; + document.forms["action"].action = '/cobbler_web/{{ what }}/modifylist/addfilter/'+field_name.value+':'+field_value.value; + document.forms["action"].submit(); } } +function del_filter(filter) { + document.forms["action"].action = '/cobbler_web/{{ what }}/modifylist/removefilter/' + filter + document.forms["action"].submit(); +} </script> <ul id="filter-adder"> <li> @@ -151,7 +157,7 @@ function add_filter() { {% if filters %} <ul id="filter-remover"> {% for key,value in filters.items %} - <li><a href="/cobbler_web/{{ what }}/modifylist/removefilter/{{ key }}" title="remove">✖</a> {{ key }} = {{ value }}</li> + <li><a href="javascript:del_filter('{{ key }}');" title="remove">✖</a> {{ key }} = {{ value }}</li> {% endfor %} </ul> {% endif %}
web/cobbler_web/templates/generic_list.tmpl+13 −4 modified@@ -2,6 +2,7 @@ {% load site %} {% block content %} +<form id="action" method="POST" action="">{% csrf_token %}</form> <script type="text/javascript"> function items_check_all(){ var checkall = document.getElementById("itemsall").checked @@ -31,18 +32,21 @@ function items_checked_values() { function obj_rename(old) { var newname = window.prompt("Change {{ what }} name to?",old); if (newname != null) { - window.location = "/cobbler_web/{{ what }}/rename/" + old + "/" + newname; + document.forms["action"].action = "/cobbler_web/{{ what }}/rename/" + old + "/" + newname; + document.forms["action"].submit(); } } function obj_copy(old) { var newname = window.prompt("Name for the new {{ what }}?",old); if (newname != null) { - window.location = "/cobbler_web/{{ what }}/copy/" + old + "/" + newname; + document.forms["action"].action = "/cobbler_web/{{ what }}/copy/" + old + "/" + newname; + document.forms["action"].submit(); } } function obj_delete(old) { if (confirm("Delete {{ what }} (" + old + ") and all child objects?")) { - window.location = "/cobbler_web/{{ what }}/delete/" + old; + document.forms["action"].action = "/cobbler_web/{{ what }}/delete/" + old; + document.forms["action"].submit(); } } @@ -53,6 +57,11 @@ function action(otype) { document.location = "/cobbler_web/" + what + "/" + action } +function action_sort(value) { + document.forms["action"].action = '/cobbler_web/{{ what }}/modifylist/sort/' + value; + document.forms["action"].submit(); +} + function action_multi(otype) { var values = items_checked_values() if (values == "") { @@ -135,7 +144,7 @@ function action_multi(otype) { </th> {% for value in columns %} <th> - <a href="/cobbler_web/{{ what }}/modifylist/sort/{{ value.0 }}">{{ value.0|title }}</a> + <a href="javascript:action_sort('{{ value.0 }}');">{{ value.0|title }}</a> {% ifequal value.1 "asc" %} ↓ {% endifequal %}
web/cobbler_web/templates/master.tmpl+13 −6 modified@@ -14,9 +14,16 @@ <div id="container"> <div id='user'> <input type="hidden" name="username" id="username" value="{{ username }}" /> - Logged in: <b>{{ username }}</b> <a class="action" href="/cobbler_web/logout">Logout</a> + Logged in: <b>{{ username }}</b> <a class="action" href="javascript:menuaction('/cobbler_web/logout');">Logout</a> </div> <div id="menubar"> + <form id="menuaction" method="POST" action="">{% csrf_token %}</form> + <script type="text/javascript"> + function menuaction(action) { + document.forms["menuaction"].action = action + document.forms["menuaction"].submit(); + } + </script> <h1>Configuration</h1> <ul> <li><a href="/cobbler_web/distro/list" class="edit">Distros</a></li> @@ -36,11 +43,11 @@ <h1>Actions</h1> <ul> <li><a href="/cobbler_web/import/prompt">Import DVD</a></li> - <li><a href="/cobbler_web/sync">Sync</a> ☼</li> - <li><a href="/cobbler_web/reposync">Reposync</a> ☼ </li> - <li><a href="/cobbler_web/hardlink">Hardlink</a> ☼ </li> - <!-- <li><a href="/cobbler_web/replicate">Replicate</a> ☼ </li> --> - <li><a href="/cobbler_web/buildiso">Build ISO</a> ☼ </li> + <li><a href="javascript:menuaction('/cobbler_web/sync');">Sync</a> ☼</li> + <li><a href="javascript:menuaction('/cobbler_web/reposync');"">Reposync</a> ☼ </li> + <li><a href="javascript:menuaction('/cobbler_web/hardlink');"">Hardlink</a> ☼ </li> + <!-- <li><a href="javascript:menuaction('/cobbler_web/replicate');"">Replicate</a> ☼ </li> --> + <li><a href="javascript:menuaction('/cobbler_web/buildiso');">Build ISO</a> ☼ </li> </ul> <h1>Cobbler</h1> <ul>
web/cobbler_web/templates/paginate.tmpl+16 −4 modified@@ -1,20 +1,32 @@ {% if pageinfo %} +<script type="text/javascript"> +// All of these functions depend on the form "action" defined in generic_list +function change_limit(value) { + document.forms["action"].action = '/cobbler_web/{{ what }}/modifylist/limit/' + value + document.forms["action"].submit(); +} +function change_page(value) { + document.forms["action"].action = '/cobbler_web/{{ what }}/modifylist/page/' + value + document.forms["action"].submit(); +} +</script> + <li class="paginate"><label for="limit">Items/page:</label> - <select name="limit" id="limit" onchange="javascript:location='/cobbler_web/{{ what }}/modifylist/limit/'+this.value"> + <select name="limit" id="limit" onchange="javascript:change_limit(this.value)"> {% for p in pageinfo.items_per_page_list %} <option value="{{ p }}"{% ifequal pageinfo.items_per_page p %} selected="selected"{% endifequal %}>{{ p }}</option> {% endfor %} </select> {% ifnotequal pageinfo.prev_page "~" %} - <a href="/cobbler_web/{{ what }}/modifylist/page/{{ pageinfo.prev_page }}"><span class="lpointers">⇐</span></a> + <a href="javascript:change_page('{{ pageinfo.prev_page }}')"><span class="lpointers">⇐</span></a> {% else %} <span class="lpointers">⇐</span> {% endifnotequal %} - <select name="page" id="page" onchange="javascript:location='/cobbler_web/{{ what }}/modifylist/page/'+this.value"> + <select name="page" id="page" onchange="javascript:change_page(this.value)"> {% for p in pageinfo.pages %}<option value="{{ p }}"{% ifequal pageinfo.page p %} selected="selected"{% endifequal %}>Page {{ p }}</option>{% endfor %} </select> {% ifnotequal pageinfo.next_page "~" %} - <a href="/cobbler_web/{{ what }}/modifylist/page/{{ pageinfo.next_page }}"><span class="rpointers">⇒</span></a> + <a href="javascript:change_page('{{ pageinfo.next_page }}')"><span class="rpointers">⇒</span></a> {% else %} <span class="rpointers">⇒</span> {% endifnotequal %}
web/cobbler_web/views.py+28 −1 modified@@ -4,6 +4,7 @@ from django.http import HttpResponse from django.http import HttpResponseRedirect from django.shortcuts import render_to_response +from django.views.decorators.http import require_POST try: from django.views.decorators.csrf import csrf_protect @@ -366,7 +367,8 @@ def genlist(request, what, page=None): })) return HttpResponse(html) - +@require_POST +@csrf_protect def modify_list(request, what, pref, value=None): """ This function is used in the generic list view @@ -433,6 +435,8 @@ def modify_list(request, what, pref, value=None): # ====================================================================== +@require_POST +@csrf_protect def generic_rename(request, what, obj_name=None, obj_newname=None): """ @@ -453,6 +457,8 @@ def generic_rename(request, what, obj_name=None, obj_newname=None): # ====================================================================== +@require_POST +@csrf_protect def generic_copy(request, what, obj_name=None, obj_newname=None): """ Copies an object. @@ -472,6 +478,8 @@ def generic_copy(request, what, obj_name=None, obj_newname=None): # ====================================================================== +@require_POST +@csrf_protect def generic_delete(request, what, obj_name=None): """ Deletes an object. @@ -491,6 +499,8 @@ def generic_delete(request, what, obj_name=None): # ====================================================================== +@require_POST +@csrf_protect def generic_domulti(request, what, multi_mode=None, multi_arg=None): """ @@ -588,13 +598,17 @@ def check(request): # ====================================================================== +@require_POST +@csrf_protect def buildiso(request): if not test_user_authenticated(request): return login(request, next="/cobbler_web/buildiso") remote.background_buildiso({},request.session['token']) return HttpResponseRedirect('/cobbler_web/task_created') # ====================================================================== +@require_POST +@csrf_protect def import_run(request): if not test_user_authenticated(request): return login(request, next="/cobbler_web/import/prompt") options = { @@ -667,6 +681,7 @@ def ksfile_edit(request, ksfile_name=None, editmode='edit'): # ====================================================================== +@require_POST @csrf_protect def ksfile_save(request): """ @@ -752,6 +767,7 @@ def snippet_edit(request, snippet_name=None, editmode='edit'): # ====================================================================== +@require_POST @csrf_protect def snippet_save(request): """ @@ -872,6 +888,8 @@ def random_mac(request, virttype="xenpv"): # ====================================================================== +@require_POST +@csrf_protect def sync(request): """ Runs 'cobbler sync' from the API when the user presses the sync button. @@ -882,6 +900,8 @@ def sync(request): # ====================================================================== +@require_POST +@csrf_protect def reposync(request): """ Syncs all repos that are configured to be synced. @@ -892,6 +912,8 @@ def reposync(request): # ====================================================================== +@require_POST +@csrf_protect def hardlink(request): """ Hardlinks files between repos and install trees to save space. @@ -902,6 +924,8 @@ def hardlink(request): # ====================================================================== +@require_POST +@csrf_protect def replicate(request): """ Replicate configuration from the central cobbler server, configured @@ -1022,6 +1046,7 @@ def generic_edit(request, what=None, obj_name=None, editmode="new"): # ====================================================================== +@require_POST @csrf_protect def generic_save(request,what): @@ -1175,6 +1200,7 @@ def test_user_authenticated(request): def login(request, next=None): return render_to_response('login.tmpl', RequestContext(request,{'next':next})) +@require_POST @csrf_protect def do_login(request): global remote @@ -1205,6 +1231,7 @@ def do_login(request): else: return login(request,nextsite) +@require_POST @csrf_protect def do_logout(request): request.session['username'] = ""
4bee30b4086aEnabling CSRF protection for the web interface
8 files changed · +68 −21
web/cobbler_web/templates/generic_edit.tmpl+1 −0 modified@@ -404,6 +404,7 @@ $(document).ready(function() <h1>{% ifequal editmode 'edit' %}Editing{% else %}Adding{% endifequal %} a {{ what|capfirst }}{%ifequal editmode 'edit' %}: {{ name }}{% endifequal %}</h1> <hr /> <form method="post" action="/cobbler_web/{{ what }}/save"> + {% csrf_token %} <input type="hidden" name="editmode" value="{{ editmode }}" /> <input type="hidden" name="subobject" value="{{ subobject }}" /> <ol>
web/cobbler_web/templates/generic_list.tmpl+1 −0 modified@@ -126,6 +126,7 @@ function action_multi(otype) { </ul> <form name="myform" method="post" action="/cobbler_web/{{ what }}/action"> + {% csrf_token %} <table id="listitems" cellspacing="0"> <thead> <tr>
web/cobbler_web/templates/import.tmpl+1 −0 modified@@ -3,6 +3,7 @@ <h1>DVD Importer</h1> <hr /> <form method="post" action="/cobbler_web/import/run"> + {% csrf_token %} <div class="sectionbody"> <ul> <li class="editrow">
web/cobbler_web/templates/ksfile_edit.tmpl+1 −0 modified@@ -17,6 +17,7 @@ <h1>{% ifequal editmode 'edit' %}Editing{% else %}Adding{% endifequal %} a Kickstart Template</h1> <hr /> <form id="ksform" method="post" action="/cobbler_web/ksfile/save"> + {% csrf_token %} <ol> <li> <label for="ksdata">{% if ksfile_name %}Editing: {{ ksfile_name }}{% else %}Filename:{% endif %}</label>
web/cobbler_web/templates/login.tmpl+1 −0 modified@@ -12,6 +12,7 @@ <p class="error">Sorry, that's not a valid username or password</p> {% endif %} <form action="/cobbler_web/do_login" method="post"> + {% csrf_token %} {% if next %}<input type="hidden" name="next" value="{{ next|escape }}" />{% endif %} <div id="username"> <label for="username">Username: </label>
web/cobbler_web/templates/snippet_edit.tmpl+1 −0 modified@@ -14,6 +14,7 @@ if you need to resolve this. <h1>{% ifequal editmode 'edit' %}Editing{% else %}Adding{% endifequal %} a Snippet</h1> <hr /> <form id="snippetform" method="post" action="/cobbler_web/snippet/save"> + {% csrf_token %} <ol> <li> <label for="snippetdata">{% if snippet_name %}Snippet: {{ snippet_name }}{% else %}Filename:{% endif %}</label>
web/cobbler_web/views.py+42 −15 modified@@ -5,6 +5,25 @@ from django.http import HttpResponseRedirect from django.shortcuts import render_to_response +try: + from django.views.decorators.csrf import csrf_protect +except: + # Old Django, fudge the @csrf_protect decorator to be a pass-through + # that does nothing. Django decorator shell based on this page: + # http://passingcuriosity.com/2009/writing-view-decorators-for-django/ + def csrf_protect(f): + def _dec(view_func): + def _view(request,*args,**kwargs): + return view_func(request,*args,**kwargs) + _view.__name__ = view_func.__name__ + _view.__dict__ = view_func.__dict__ + _view.__doc__ = view_func.__doc__ + return _view + if f is None: + return _dec + else: + return _dec(f) + import xmlrpclib import time import simplejson @@ -37,7 +56,7 @@ def index(request): if not test_user_authenticated(request): return login(request,next="/cobbler_web") t = get_template('index.tmpl') - html = t.render(Context({ + html = t.render(RequestContext(request,{ 'version' : remote.version(request.session['token']), 'username': username, })) @@ -51,7 +70,7 @@ def task_created(request): """ if not test_user_authenticated(request): return login(request, next="/cobbler_web/task_created") t = get_template("task_created.tmpl") - html = t.render(Context({ + html = t.render(RequestContext(request,{ 'version' : remote.version(request.session['token']), 'username' : username })) @@ -69,7 +88,7 @@ def error_page(request,message): t = get_template('error_page.tmpl') message = message.replace("<Fault 1: \"<class 'cobbler.cexceptions.CX'>:'","Remote exception: ") message = message.replace("'\">","") - html = t.render(Context({ + html = t.render(RequestContext(request,{ 'version' : remote.version(request.session['token']), 'message' : message, 'username': username @@ -545,7 +564,7 @@ def generic_domulti(request, what, multi_mode=None, multi_arg=None): def import_prompt(request): if not test_user_authenticated(request): return login(request, next="/cobbler_web/import/prompt") t = get_template('import.tmpl') - html = t.render(Context({ + html = t.render(RequestContext(request,{ 'version' : remote.version(request.session['token']), 'username' : username, })) @@ -560,7 +579,7 @@ def check(request): if not test_user_authenticated(request): return login(request, next="/cobbler_web/check") results = remote.check(request.session['token']) t = get_template('check.tmpl') - html = t.render(Context({ + html = t.render(RequestContext(request,{ 'version': remote.version(request.session['token']), 'username' : username, 'results' : results @@ -606,7 +625,7 @@ def ksfile_list(request, page=None): ksfile_list.append((ksfile,ksfile,None)) t = get_template('ksfile_list.tmpl') - html = t.render(Context({ + html = t.render(RequestContext(request,{ 'what':'ksfile', 'ksfiles': ksfile_list, 'version': remote.version(request.session['token']), @@ -617,7 +636,7 @@ def ksfile_list(request, page=None): # ====================================================================== - +@csrf_protect def ksfile_edit(request, ksfile_name=None, editmode='edit'): """ This is the page where a kickstart file is edited. @@ -635,7 +654,7 @@ def ksfile_edit(request, ksfile_name=None, editmode='edit'): ksdata = remote.read_or_write_kickstart_template(ksfile_name, True, "", request.session['token']) t = get_template('ksfile_edit.tmpl') - html = t.render(Context({ + html = t.render(RequestContext(request,{ 'ksfile_name' : ksfile_name, 'deleteable' : deleteable, 'ksdata' : ksdata, @@ -648,6 +667,7 @@ def ksfile_edit(request, ksfile_name=None, editmode='edit'): # ====================================================================== +@csrf_protect def ksfile_save(request): """ This page processes and saves edits to a kickstart file. @@ -691,7 +711,7 @@ def snippet_list(request, page=None): snippet_list.append((snippet,snippet,None)) t = get_template('snippet_list.tmpl') - html = t.render(Context({ + html = t.render(RequestContext(request,{ 'what' : 'snippet', 'snippets' : snippet_list, 'version' : remote.version(request.session['token']), @@ -701,6 +721,7 @@ def snippet_list(request, page=None): # ====================================================================== +@csrf_protect def snippet_edit(request, snippet_name=None, editmode='edit'): """ This page edits a specific snippet. @@ -718,7 +739,7 @@ def snippet_edit(request, snippet_name=None, editmode='edit'): snippetdata = remote.read_or_write_snippet(snippet_name, True, "", request.session['token']) t = get_template('snippet_edit.tmpl') - html = t.render(Context({ + html = t.render(RequestContext(request,{ 'snippet_name' : snippet_name, 'deleteable' : deleteable, 'snippetdata' : snippetdata, @@ -731,6 +752,7 @@ def snippet_edit(request, snippet_name=None, editmode='edit'): # ====================================================================== +@csrf_protect def snippet_save(request): """ This snippet saves a snippet once edited. @@ -774,7 +796,7 @@ def settings(request): results.append([k,settings[k]]) t = get_template('settings.tmpl') - html = t.render(Context({ + html = t.render(RequestContext(request,{ 'settings' : results, 'version' : remote.version(request.session['token']), 'username' : username, @@ -800,7 +822,7 @@ def sorter(a,b): events2.sort(sorter) t = get_template('events.tmpl') - html = t.render(Context({ + html = t.render(RequestContext(request,{ 'results' : events2, 'version' : remote.version(request.session['token']), 'username' : username @@ -834,7 +856,7 @@ def eventlog(request, event=0): 'version' : remote.version(request.session['token']), 'username' : username } - html = t.render(Context(vars)) + html = t.render(RequestContext(request,vars)) return HttpResponse(html) # ====================================================================== @@ -912,6 +934,7 @@ def __names_from_dicts(loh,optional=True): # ====================================================================== +@csrf_protect def generic_edit(request, what=None, obj_name=None, editmode="new"): """ @@ -980,7 +1003,7 @@ def generic_edit(request, what=None, obj_name=None, editmode="new"): t = get_template('generic_edit.tmpl') inames = interfaces.keys() inames.sort() - html = t.render(Context({ + html = t.render(RequestContext(request,{ 'what' : what, 'fields' : fields, 'subobject' : child, @@ -998,6 +1021,7 @@ def generic_edit(request, what=None, obj_name=None, editmode="new"): # ====================================================================== +@csrf_protect def generic_save(request,what): """ @@ -1146,9 +1170,11 @@ def test_user_authenticated(request): pass return False +@csrf_protect def login(request, next=None): - return render_to_response('login.tmpl', {'next':next}) + return render_to_response('login.tmpl', RequestContext(request,{'next':next})) +@csrf_protect def do_login(request): global remote global username @@ -1178,6 +1204,7 @@ def do_login(request): else: return login(request,nextsite) +@csrf_protect def do_logout(request): request.session['username'] = "" request.session['token'] = ""
web/settings.py+20 −6 modified@@ -1,4 +1,5 @@ # Django settings for cobbler-web project. +import django DEBUG = True TEMPLATE_DEBUG = DEBUG @@ -39,12 +40,25 @@ 'django.template.loaders.filesystem.load_template_source', 'django.template.loaders.app_directories.load_template_source', ) -MIDDLEWARE_CLASSES = ( - 'django.middleware.common.CommonMiddleware', - 'django.contrib.csrf.middleware.CsrfMiddleware', - 'django.contrib.sessions.middleware.SessionMiddleware', - 'django.contrib.auth.middleware.AuthenticationMiddleware', -) + +if django.VERSION[0] == 1 and django.VERSION[1] < 2: + # Legacy django had a different CSRF method, which also had + # different middleware. We check the vesion here so we bring in + # the correct one. + MIDDLEWARE_CLASSES = ( + 'django.middleware.common.CommonMiddleware', + 'django.contrib.csrf.middleware.CsrfMiddleware', + 'django.contrib.sessions.middleware.SessionMiddleware', + 'django.contrib.auth.middleware.AuthenticationMiddleware', + ) +else: + MIDDLEWARE_CLASSES = ( + 'django.middleware.common.CommonMiddleware', + 'django.middleware.csrf.CsrfViewMiddleware', + 'django.contrib.sessions.middleware.SessionMiddleware', + 'django.contrib.auth.middleware.AuthenticationMiddleware', + ) + ROOT_URLCONF = 'urls' TEMPLATE_DIRS = (
Vulnerability mechanics
Generated on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
8- github.com/advisories/GHSA-9fqr-pqc9-f7pjghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2011-4952ghsaADVISORY
- www.openwall.com/lists/oss-security/2012/04/12/10ghsax_refsource_MISCWEB
- access.redhat.com/security/cve/cve-2011-4952ghsax_refsource_MISCWEB
- bugzilla.redhat.com/show_bug.cgighsax_refsource_MISCWEB
- github.com/cobbler/cobbler/commit/18eb1c06779b37d89dfb2962a08236dd1bab24a6ghsaWEB
- github.com/cobbler/cobbler/commit/4bee30b4086a8d845bea5d39d6f2cba1f4a396aaghsaWEB
- security-tracker.debian.org/tracker/CVE-2011-4952ghsax_refsource_MISCWEB
News mentions
0No linked articles in our index yet.