Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions changelog.d/3465.security.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Add CSRF token to ajax post requests
122 changes: 122 additions & 0 deletions doc/hacking/javascript.rst
Original file line number Diff line number Diff line change
Expand Up @@ -100,3 +100,125 @@ only be global configuration files for *RequireJS*, *jshint*, etc.

:file:`src/plugins/`
contains re-usable JavaScript plugins.

CSRF Token Handling
===================

When making AJAX requests that modify data (POST, PUT, DELETE), you must include Django's CSRF token for security.

Getting the CSRF Token
-----------------------

**Method 1: From a hidden form (Recommended)**

.. code-block:: javascript

const csrfToken = $('#some-form-id input[name="csrfmiddlewaretoken"]').val();

**Method 2: From any form on the page**

.. code-block:: javascript

const csrfToken = $('[name=csrfmiddlewaretoken]').val();

Using CSRF Tokens in AJAX Requests
-----------------------------------

There are three ways to include a CSRF Token in the requests.

**Method 1: With jQuery POST data object:**

This method includes the CSRF token directly in the POST data. This is the most straightforward approach when you have simple form data.

.. code-block:: javascript

$.post({
url: '/some/endpoint/',
data: {
'field': 'value',
'csrfmiddlewaretoken': csrfToken
}
});

**Method 2: With jQuery headers:**

This method sends the CSRF token in the HTTP headers using Django's expected header name. This is useful when posting complex data like FormData objects or JSON.

.. code-block:: javascript

$.post({
url: '/some/endpoint/',
data: formData,
headers: {
'X-CSRFToken': csrfToken
}
});

**Method 3: With serialized form data:**

This method does not require getting the token from the template explicitly, but is done as part of native HTML form processing. The CSRF token is automatically included when the form is serialized.

.. code-block:: javascript

// If posting a complete form, the token is included automatically
$.post(url, $('#my-form').serialize());

Including CSRF Token in Templates
----------------------------------

Django templates provide the ``{% csrf_token %}`` template tag to automatically include the CSRF token in forms. This is the recommended approach for standard form submissions.

**Basic form with CSRF token:**

This is the most common pattern for regular form submissions. The CSRF token is included automatically when the form is submitted normally.

.. code-block:: html

<form method="post" action="{% url 'some-endpoint' %}">
{% csrf_token %}
<input type="text" name="field_name" value="">
<input type="submit" value="Submit">
</form>

**Hidden form for JavaScript access:**

This pattern creates a hidden form solely to provide JavaScript access to the CSRF token. This is useful when you need to make AJAX requests from JavaScript but don't have a visible form on the page.

.. code-block:: html

<form id="example-form" style="display: none;">
{% csrf_token %}
</form>

**Multiple forms on the same page:**

When you have multiple forms that perform different actions, each form needs its own CSRF token. This example shows two example forms for resource operations - one for renaming and one for deleting.

.. code-block:: html

<form id="form-rename-resource" method="post" action="{% url 'rename-resource' resource.pk %}">
{% csrf_token %}
<input type="text" name="resource-name" value="{{ resource.name }}">
<input type="submit" value="Rename resource">
</form>

<form id="form-delete-resource" method="post" action="{% url 'delete-resource' resource.pk %}">
{% csrf_token %}
<input type="submit" value="Delete resource">
</form>

**HTMX forms with CSRF token:**

When using HTMX for dynamic content updates, the CSRF token is still required for POST requests. HTMX will automatically include the token from the form when making the request.

.. code-block:: html

<form method="post"
hx-post="{% url 'some-endpoint' %}"
hx-target="#result-container">
{% csrf_token %}
<input type="text" name="data">
<button type="submit">Submit</button>
</form>

The ``{% csrf_token %}`` tag renders as a hidden input field with name ``csrfmiddlewaretoken`` that JavaScript can access to include in AJAX requests.
3 changes: 2 additions & 1 deletion python/nav/web/static/js/src/neighbors.js
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,8 @@ require(['libs/datatables.min'], function() {

var request = $.post(setIgnoredUrl, {
neighborids: neighborids,
action: action
action: action,
csrfmiddlewaretoken: $('[name=csrfmiddlewaretoken]').val()
});

$feedback.hide();
Expand Down
14 changes: 13 additions & 1 deletion python/nav/web/static/js/src/netmap/control_view.js
Original file line number Diff line number Diff line change
Expand Up @@ -229,9 +229,18 @@ define([
this.currentView.baseZoom = this.currentView.get('zoom');
var isNew = this.currentView.isNew();

// Get CSRF token from the appropriate form
const csrfToken = isNew
? $('#netmap-view-create-form input[name=csrfmiddlewaretoken]').val()
: $('#netmap-view-edit-form input[name=csrfmiddlewaretoken]').val();
this.currentView.set('csrfmiddlewaretoken', csrfToken);

var self = this;
this.currentView.save(this.currentView.attributes,
{
beforeSend: function(xhr) {
xhr.setRequestHeader('X-CSRFToken', csrfToken);
},
success: function (model) {
Backbone.EventBroker.trigger('netmap:saveNodePositions', model, {'isNew': isNew}, self.middleAlertContainer);
},
Expand Down Expand Up @@ -294,8 +303,11 @@ define([
if(confirm('Delete this view?')) {
if (!this.currentView.isNew()) {
var self = this;
console.log('We want to delete view with id ' + this.currentView.id);
const csrfToken = $('#netmap-view-delete-form input[name=csrfmiddlewaretoken]').val();
this.currentView.destroy({
beforeSend: function(xhr) {
xhr.setRequestHeader('X-CSRFToken', csrfToken);
},
success: function () {
self.deleteSuccessful.call(self, false);
},
Expand Down
11 changes: 11 additions & 0 deletions python/nav/web/static/js/src/netmap/models.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,17 @@ define([

url: function () {
return 'views/' + this.get('viewid') + '/nodepositions/update/';
},

save: function (attrs, options) {
options = options || {};
const csrfToken = $('#netmap-view-settings-form input[name="csrfmiddlewaretoken"]').val();

options.headers = {
...options.headers,
'X-CSRFToken': csrfToken
}
return Backbone.Model.prototype.save.call(this, attrs, options);
}
});

Expand Down
12 changes: 9 additions & 3 deletions python/nav/web/static/js/src/plugins/navlet_controller.js
Original file line number Diff line number Diff line change
Expand Up @@ -250,14 +250,20 @@ define(['libs/urijs/URI', 'libs/spin.min'], function (URI, Spinner) {
$container.append($input);
$input.on('keydown', function (event) {
if (event.which === 13) {
var request = $.post($header.attr('data-set-title'),
{
const csrfToken = $('#navlets-action-form input[name="csrfmiddlewaretoken"]').val();
const request = $.post({
url: $header.attr('data-set-title'),
type: 'POST',
data: {
'id': self.navlet.id,
'preferences': JSON.stringify({
'title': $input.val()
})
},
headers: {
'X-CSRFToken': csrfToken
}
);
});
request.done(function () {
$header.find('.navlet-title').text($input.val());
});
Expand Down
14 changes: 13 additions & 1 deletion python/nav/web/static/js/src/plugins/navlets_controller.js
Original file line number Diff line number Diff line change
Expand Up @@ -148,7 +148,19 @@ define(['plugins/navlet_controller'], function (NavletController) {
return orderings;
},
saveOrder: function (ordering) {
$.post(this.save_ordering_url, JSON.stringify(ordering));
// Get csrf token from #navlets-action-form
const csrfToken = $('#navlets-action-form input[name="csrfmiddlewaretoken"]').val();
$.ajax({
url: this.save_ordering_url,
type: 'POST',
data: JSON.stringify(ordering),
contentType: 'application/json',
headers: {
'X-CSRFToken': csrfToken
}
}).fail(function() {
console.error('Failed to save widget order');
});
},
getNavlets: function (column) {
if (column) {
Expand Down
18 changes: 15 additions & 3 deletions python/nav/web/static/js/src/portadmin.js
Original file line number Diff line number Diff line change
Expand Up @@ -346,6 +346,7 @@ require(['libs/spin.min', 'libs/jquery-ui.min'], function (Spinner) {
var $row = $('#' + rowid);
var interfaceData = queue_data[rowid];
var listItem = feedback.savingInterface($row);
const csrfToken = $('#save-changes-form [name="csrfmiddlewaretoken"]').val();
$.ajax({url: "save_interfaceinfo",
data: interfaceData,
dataType: 'json',
Expand All @@ -354,6 +355,9 @@ require(['libs/spin.min', 'libs/jquery-ui.min'], function (Spinner) {
disableButtons($row);
// spinner.spin($row);
},
headers: {
'X-CSRFToken': csrfToken
},
success: function () {
clearChangedState($row);
updateDefaults($row, interfaceData);
Expand Down Expand Up @@ -400,13 +404,21 @@ require(['libs/spin.min', 'libs/jquery-ui.min'], function (Spinner) {
/** Do a request to commit changes to startup config */
console.log('Sending commit configuration request');

var status = feedback.committingConfig();
var request = $.post('commit_configuration', {'interfaceid': interfaceid});
const status = feedback.committingConfig();
const csrfToken = $('#save-changes-form input[name="csrfmiddlewaretoken"]').val();
const request = $.ajax({
url: 'commit_configuration',
type: 'POST',
data: {'interfaceid': interfaceid},
headers: {
'X-CSRFToken': csrfToken
},
});
request.done(function() {
feedback.endProgress(status, 'success', request.responseText);
restartInterfaces();
});
request.fail(function() {
request.fail(function(err) {
feedback.endProgress(status, 'alert', request.responseText);
feedback.addCloseButton();
});
Expand Down
16 changes: 9 additions & 7 deletions python/nav/web/static/js/src/status2/views.js
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ define([
var self = this;
var request = $.post(
NAV.urls.status2_save_preferences,
this.$el.find('form').serialize()
this.$el.find('#status-panel form').serialize()
);

self.setDefaultButton.removeClass('alert'); // Remove errorclass if an error occured on last try
Expand Down Expand Up @@ -275,12 +275,12 @@ define([
},

acknowledgeAlerts: function () {
console.log('acknowledgeAlerts');
var self = this;

var request = $.post(NAV.urls.status2_acknowledge_alert, {
id: alertsToChange.pluck('id'),
comment: this.comment.val()
comment: this.comment.val(),
csrfmiddlewaretoken: $('#action-panel-revised [name=csrfmiddlewaretoken]').val()
});

request.done(function () {
Expand All @@ -299,7 +299,8 @@ define([
var self = this;

var request = $.post(NAV.urls.status2_clear_alert, {
id: alertsToChange.pluck('id')
id: alertsToChange.pluck('id'),
csrfmiddlewaretoken: $('#action-panel-revised [name=csrfmiddlewaretoken]').val()
});

request.done(function () {
Expand Down Expand Up @@ -327,7 +328,8 @@ define([
if (ids.length > 0) {
var request = $.post(NAV.urls.status2_put_on_maintenance, {
id: ids,
description: description
description: description,
csrfmiddlewaretoken: this.$('[name=csrfmiddlewaretoken]').val()
});

request.done(function () {
Expand Down Expand Up @@ -358,7 +360,8 @@ define([
if (ids.length > 0) {
var request = $.post(NAV.urls.status2_delete_module_or_chassis, {
id: ids,
description: description
description: description,
csrfmiddlewaretoken: this.$('[name=csrfmiddlewaretoken]').val()
});

request.done(function () {
Expand All @@ -368,7 +371,6 @@ define([
});

request.fail(function () {
console.log(request);
self.give_error_feedback('Error deleting module or chassis');
});
} else {
Expand Down
17 changes: 13 additions & 4 deletions python/nav/web/static/js/src/webfront.js
Original file line number Diff line number Diff line change
Expand Up @@ -195,11 +195,17 @@ require([
function addColumnListener() {
$('.column-chooser').click(function () {
$navletsContainer.empty();
var columns = $(this).data('columns');
const columns = $(this).data('columns');
new NavletsController($navletsContainer, columns);
// Save number of columns
var url = $(this).closest('.button-group').data('url');
var request = $.post(url, {num_columns: columns});
const url = $(this).closest('.button-group').data('url');
const csrfToken = $('#update-columns-form input[name=csrfmiddlewaretoken]').val();
const request = $.ajax({
url,
type: 'POST',
data: {num_columns: columns},
headers: {'X-CSRFToken': csrfToken}
});
request.done(function () {
$navletsContainer.data('widget-columns', columns);
});
Expand All @@ -223,7 +229,10 @@ require([
setDefaultDashboardForm.submit(function (event) {
event.preventDefault();
feedback.removeAlertbox();
var request = $.post(this.getAttribute('action'));
const request = $.post(
this.getAttribute('action'),
$(this).serialize()
);
request.done(function (responseText) {
feedback.addFeedback(responseText);
setDefaultDashboardForm.hide();
Expand Down
7 changes: 5 additions & 2 deletions python/nav/web/templates/neighbors/base.html
Original file line number Diff line number Diff line change
Expand Up @@ -23,13 +23,15 @@
</div>
</div>

<form method="POST">
{% csrf_token %}
<div>
<ul id="action-buttons" class="button-group">
<li>
<button id="ignore-selected" class="tiny">Ignore selected</button>
<button id="ignore-selected" class="tiny" type="button">Ignore selected</button>
</li>
<li>
<button id="unignore-selected" class="tiny">Unignore selected</button>
<button id="unignore-selected" class="tiny" type="button">Unignore selected</button>
</li>
</ul>

Expand Down Expand Up @@ -63,4 +65,5 @@
{% include 'neighbors/frag-tbody.html' %}
</tbody>
</table>
</form>
{% endblock %}
Loading
Loading