337 lines
13 KiB
JavaScript
337 lines
13 KiB
JavaScript
/**
|
|
* CSRF Guard - Universal Cross-Site Request Forgery Protection Handler
|
|
*
|
|
* Automatically protects ALL HTTP requests across the application:
|
|
* 1. jQuery AJAX ($.ajax, $.post, $.get with payload)
|
|
* 2. Fetch API (window.fetch)
|
|
* 3. XMLHttpRequest (raw XHR, axios, third-party libraries)
|
|
* 4. HTML Forms (native submit & programmatic form.submit())
|
|
*/
|
|
(function(window, document) {
|
|
'use strict';
|
|
|
|
var DEFAULT_TOKEN_NAME = 'csrf_test_name';
|
|
var DEFAULT_HEADER_NAME = 'X-CSRF-TOKEN';
|
|
|
|
var currentToken = '';
|
|
var tokenName = DEFAULT_TOKEN_NAME;
|
|
var headerName = DEFAULT_HEADER_NAME;
|
|
|
|
function getCookie(name) {
|
|
if (!document.cookie) return null;
|
|
var parts = document.cookie.split(';');
|
|
for (var i = 0; i < parts.length; i++) {
|
|
var c = parts[i].trim();
|
|
if (c.indexOf(name + '=') === 0) {
|
|
return decodeURIComponent(c.substring(name.length + 1));
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
function readMetaTags() {
|
|
var metaToken = document.querySelector('meta[name="csrf-token"]') ||
|
|
document.querySelector('meta[name="X-CSRF-TOKEN"]') ||
|
|
document.querySelector('meta[name="csrf-hash"]');
|
|
if (metaToken && metaToken.getAttribute('content')) {
|
|
currentToken = metaToken.getAttribute('content');
|
|
}
|
|
|
|
var metaName = document.querySelector('meta[name="csrf-token-name"]');
|
|
if (metaName && metaName.getAttribute('content')) {
|
|
tokenName = metaName.getAttribute('content');
|
|
}
|
|
|
|
var metaHeader = document.querySelector('meta[name="csrf-header"]');
|
|
if (metaHeader && metaHeader.getAttribute('content')) {
|
|
headerName = metaHeader.getAttribute('content');
|
|
}
|
|
|
|
if (!currentToken) {
|
|
var cookieToken = getCookie('csrf_cookie_name') || getCookie('csrf_test_name');
|
|
if (cookieToken) {
|
|
currentToken = cookieToken;
|
|
}
|
|
}
|
|
}
|
|
|
|
readMetaTags();
|
|
|
|
function updateToken(newToken) {
|
|
if (!newToken || typeof newToken !== 'string' || newToken === currentToken) {
|
|
return;
|
|
}
|
|
currentToken = newToken;
|
|
|
|
// Update meta tag
|
|
var metaToken = document.querySelector('meta[name="csrf-token"]') ||
|
|
document.querySelector('meta[name="X-CSRF-TOKEN"]') ||
|
|
document.querySelector('meta[name="csrf-hash"]');
|
|
if (metaToken) {
|
|
metaToken.setAttribute('content', newToken);
|
|
}
|
|
|
|
// Update all existing hidden inputs across DOM
|
|
var inputs = document.querySelectorAll('input[name="' + tokenName + '"]');
|
|
for (var i = 0; i < inputs.length; i++) {
|
|
inputs[i].value = newToken;
|
|
}
|
|
|
|
// Update global jQuery ajaxSetup if available
|
|
if (window.jQuery && window.jQuery.ajaxSetup) {
|
|
var headers = {};
|
|
headers[headerName] = newToken;
|
|
window.jQuery.ajaxSetup({ headers: headers });
|
|
}
|
|
}
|
|
|
|
var CsrfGuard = {
|
|
getToken: function() {
|
|
if (!currentToken) readMetaTags();
|
|
return currentToken;
|
|
},
|
|
getTokenName: function() {
|
|
return tokenName;
|
|
},
|
|
getHeaderName: function() {
|
|
return headerName;
|
|
},
|
|
updateToken: updateToken,
|
|
init: function(config) {
|
|
if (config) {
|
|
if (config.token) currentToken = config.token;
|
|
if (config.tokenName) tokenName = config.tokenName;
|
|
if (config.headerName) headerName = config.headerName;
|
|
}
|
|
readMetaTags();
|
|
this.setupJQuery();
|
|
},
|
|
setupJQuery: function() {
|
|
if (!window.jQuery) return;
|
|
var $ = window.jQuery;
|
|
|
|
if ($._csrfGuardInstalled) return;
|
|
$._csrfGuardInstalled = true;
|
|
|
|
var defaultHeaders = {};
|
|
if (currentToken) {
|
|
defaultHeaders[headerName] = currentToken;
|
|
}
|
|
$.ajaxSetup({
|
|
headers: defaultHeaders
|
|
});
|
|
|
|
$.ajaxPrefilter(function(options, originalOptions, jqXHR) {
|
|
var method = (options.type || options.method || 'GET').toUpperCase();
|
|
if (method === 'GET' || method === 'HEAD' || method === 'OPTIONS') {
|
|
return;
|
|
}
|
|
|
|
var token = CsrfGuard.getToken();
|
|
if (!token) return;
|
|
|
|
// Set header on every non-GET request
|
|
jqXHR.setRequestHeader(headerName, token);
|
|
|
|
// Inject into payload data as well
|
|
if (options.data === undefined || options.data === null) {
|
|
options.data = encodeURIComponent(tokenName) + '=' + encodeURIComponent(token);
|
|
} else if (typeof options.data === 'string') {
|
|
if (options.data.indexOf(tokenName + '=') === -1) {
|
|
options.data += (options.data.length > 0 ? '&' : '') +
|
|
encodeURIComponent(tokenName) + '=' + encodeURIComponent(token);
|
|
}
|
|
} else if (window.FormData && options.data instanceof window.FormData) {
|
|
if (!options.data.has(tokenName)) {
|
|
options.data.append(tokenName, token);
|
|
}
|
|
} else if (typeof options.data === 'object') {
|
|
options.data[tokenName] = token;
|
|
}
|
|
});
|
|
|
|
$(document).ajaxComplete(function(event, xhr, settings) {
|
|
var respToken = xhr.getResponseHeader(headerName) ||
|
|
xhr.getResponseHeader('X-CSRF-TOKEN') ||
|
|
xhr.getResponseHeader('x-csrf-token');
|
|
if (respToken) {
|
|
updateToken(respToken);
|
|
}
|
|
|
|
try {
|
|
if (xhr.responseJSON && xhr.responseJSON.csrf_hash) {
|
|
updateToken(xhr.responseJSON.csrf_hash);
|
|
}
|
|
} catch (e) {}
|
|
});
|
|
}
|
|
};
|
|
|
|
window.CSRF_GUARD = CsrfGuard;
|
|
window.getCsrfToken = CsrfGuard.getToken;
|
|
window.getCsrfTokenName = CsrfGuard.getTokenName;
|
|
window.getCsrfHeader = CsrfGuard.getHeaderName;
|
|
window.updateCsrfToken = CsrfGuard.updateToken;
|
|
|
|
// 1. Hook HTML Form Submissions
|
|
document.addEventListener('submit', function(e) {
|
|
var form = e.target;
|
|
if (!form || form.tagName !== 'FORM') return;
|
|
|
|
var method = (form.getAttribute('method') || 'GET').toUpperCase();
|
|
if (method !== 'POST') return;
|
|
|
|
var token = CsrfGuard.getToken();
|
|
if (!token) return;
|
|
|
|
var input = form.querySelector('input[name="' + tokenName + '"]');
|
|
if (!input) {
|
|
input = document.createElement('input');
|
|
input.type = 'hidden';
|
|
input.name = tokenName;
|
|
input.value = token;
|
|
form.appendChild(input);
|
|
} else {
|
|
input.value = token;
|
|
}
|
|
}, true);
|
|
|
|
// Hook programmatic form.submit()
|
|
if (window.HTMLFormElement && window.HTMLFormElement.prototype) {
|
|
var originalSubmit = window.HTMLFormElement.prototype.submit;
|
|
window.HTMLFormElement.prototype.submit = function() {
|
|
var method = (this.getAttribute('method') || 'GET').toUpperCase();
|
|
if (method === 'POST') {
|
|
var token = CsrfGuard.getToken();
|
|
if (token) {
|
|
var input = this.querySelector('input[name="' + tokenName + '"]');
|
|
if (!input) {
|
|
input = document.createElement('input');
|
|
input.type = 'hidden';
|
|
input.name = tokenName;
|
|
input.value = token;
|
|
this.appendChild(input);
|
|
} else {
|
|
input.value = token;
|
|
}
|
|
}
|
|
}
|
|
return originalSubmit.apply(this, arguments);
|
|
};
|
|
}
|
|
|
|
// 2. Hook Fetch API
|
|
if (window.fetch) {
|
|
var originalFetch = window.fetch;
|
|
window.fetch = function(resource, init) {
|
|
init = init || {};
|
|
var method = (init.method || 'GET').toUpperCase();
|
|
|
|
if (method !== 'GET' && method !== 'HEAD' && method !== 'OPTIONS') {
|
|
var token = CsrfGuard.getToken();
|
|
if (token) {
|
|
// Inject Header
|
|
if (!init.headers) {
|
|
init.headers = {};
|
|
}
|
|
|
|
if (window.Headers && init.headers instanceof Headers) {
|
|
if (!init.headers.has(headerName)) {
|
|
init.headers.set(headerName, token);
|
|
}
|
|
} else if (Array.isArray(init.headers)) {
|
|
init.headers.push([headerName, token]);
|
|
} else {
|
|
init.headers[headerName] = token;
|
|
}
|
|
|
|
// Inject Body (FormData or URLSearchParams)
|
|
if (window.FormData && init.body instanceof FormData) {
|
|
if (!init.body.has(tokenName)) {
|
|
init.body.append(tokenName, token);
|
|
}
|
|
} else if (typeof init.body === 'string') {
|
|
var ct = '';
|
|
if (init.headers) {
|
|
if (window.Headers && init.headers instanceof Headers) {
|
|
ct = init.headers.get('Content-Type') || '';
|
|
} else {
|
|
ct = init.headers['Content-Type'] || init.headers['content-type'] || '';
|
|
}
|
|
}
|
|
if (ct.indexOf('application/x-www-form-urlencoded') !== -1 && init.body.indexOf(tokenName + '=') === -1) {
|
|
init.body += (init.body.length ? '&' : '') +
|
|
encodeURIComponent(tokenName) + '=' + encodeURIComponent(token);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return originalFetch.apply(this, [resource, init]).then(function(response) {
|
|
var newToken = response.headers.get(headerName) ||
|
|
response.headers.get('X-CSRF-TOKEN') ||
|
|
response.headers.get('x-csrf-token');
|
|
if (newToken) {
|
|
updateToken(newToken);
|
|
}
|
|
return response;
|
|
});
|
|
};
|
|
}
|
|
|
|
// 3. Hook Raw XMLHttpRequest
|
|
if (window.XMLHttpRequest) {
|
|
var originalOpen = XMLHttpRequest.prototype.open;
|
|
var originalSend = XMLHttpRequest.prototype.send;
|
|
|
|
XMLHttpRequest.prototype.open = function(method, url) {
|
|
this._csrfMethod = (method || 'GET').toUpperCase();
|
|
return originalOpen.apply(this, arguments);
|
|
};
|
|
|
|
XMLHttpRequest.prototype.send = function(body) {
|
|
if (this._csrfMethod && this._csrfMethod !== 'GET' && this._csrfMethod !== 'HEAD' && this._csrfMethod !== 'OPTIONS') {
|
|
var token = CsrfGuard.getToken();
|
|
if (token) {
|
|
try {
|
|
this.setRequestHeader(headerName, token);
|
|
} catch (e) {}
|
|
}
|
|
}
|
|
|
|
var self = this;
|
|
var onReadyHandler = function() {
|
|
if (self.readyState === 4) {
|
|
try {
|
|
var newToken = self.getResponseHeader(headerName) ||
|
|
self.getResponseHeader('X-CSRF-TOKEN') ||
|
|
self.getResponseHeader('x-csrf-token');
|
|
if (newToken) {
|
|
updateToken(newToken);
|
|
}
|
|
} catch (e) {}
|
|
}
|
|
};
|
|
this.addEventListener('readystatechange', onReadyHandler, false);
|
|
|
|
return originalSend.apply(this, arguments);
|
|
};
|
|
}
|
|
|
|
// Try setting up jQuery now, or poll briefly until jQuery is loaded
|
|
CsrfGuard.setupJQuery();
|
|
if (!window.jQuery) {
|
|
var attempts = 0;
|
|
var checkJq = setInterval(function() {
|
|
attempts++;
|
|
if (window.jQuery) {
|
|
CsrfGuard.setupJQuery();
|
|
clearInterval(checkJq);
|
|
} else if (attempts > 50) {
|
|
clearInterval(checkJq);
|
|
}
|
|
}, 100);
|
|
}
|
|
|
|
})(window, document);
|