mirror of
https://github.com/gristlabs/grist-core.git
synced 2026-03-02 04:09:24 +00:00
(core) mitigate csrf by requiring custom header for unsafe methods
Summary: For methods other than `GET`, `HEAD`, and `OPTIONS`, allow cookie-based authentication only if a certain custom header is present. Specifically, we check that `X-Requested-With` is set to `XMLHttpRequest`. This is somewhat arbitrary, but allows us to use https://expressjs.com/en/api.html#req.xhr. A request send from a browser that sets a custom header will prompt a preflight check, giving us a chance to check if the origin is trusted. This diff deals with getting the header in place. There will be more work to do after this: * Make sure that all important endpoints are checking origin. Skimming code, /api endpoint check origin, and some but not all others. * Add tests spot-testing origin checks. * Check on cases that authenticate differently. - Check the websocket endpoint - it can be connected to from an arbitrary site; there is per-doc access control but probably better to lock it down more. - There may be old endpoints that authenticate based on knowledge of a client id rather than cookies. Test Plan: added a test Reviewers: dsagal Reviewed By: dsagal Differential Revision: https://phab.getgrist.com/D2631
This commit is contained in:
@@ -119,6 +119,7 @@ export async function uploadFiles(
|
||||
return new Promise<UploadResult>((resolve, reject) => {
|
||||
const xhr = new XMLHttpRequest();
|
||||
xhr.open('post', docUrl(options.docWorkerUrl, UPLOAD_URL_PATH), true);
|
||||
xhr.setRequestHeader('X-Requested-With', 'XMLHttpRequest');
|
||||
xhr.withCredentials = true;
|
||||
xhr.upload.addEventListener('progress', (e) => {
|
||||
if (e.lengthComputable) {
|
||||
@@ -170,3 +171,33 @@ export async function fetchURL(
|
||||
const res = await uploadFiles([fileObj], {docWorkerUrl: docComm.docWorkerUrl}, onProgress);
|
||||
return res!;
|
||||
}
|
||||
|
||||
// Submit a form using XHR. Send inputs as JSON, and interpret any reply as JSON.
|
||||
export async function submitForm(form: HTMLFormElement): Promise<any> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const xhr = new XMLHttpRequest();
|
||||
const data: {[key: string]: string} = {};
|
||||
for (const element of [...form.getElementsByTagName('input')]) {
|
||||
data[element.name] = element.value;
|
||||
}
|
||||
xhr.open('post', form.action, true);
|
||||
xhr.setRequestHeader('Content-Type', 'application/json');
|
||||
xhr.setRequestHeader('X-Requested-With', 'XMLHttpRequest');
|
||||
xhr.withCredentials = true;
|
||||
xhr.send(JSON.stringify(data));
|
||||
xhr.addEventListener('error', (e: ProgressEvent) => {
|
||||
console.warn("Form error", e); // tslint:disable-line:no-console
|
||||
reject(new Error('Form error, please try again'));
|
||||
});
|
||||
xhr.addEventListener('load', () => {
|
||||
if (xhr.status !== 200) {
|
||||
// tslint:disable-next-line:no-console
|
||||
console.warn("Form failed", xhr.status, xhr.responseText);
|
||||
const err = safeJsonParse(xhr.responseText, null);
|
||||
reject(new UserError('Form failed: ' + (err && err.error || xhr.status)));
|
||||
} else {
|
||||
resolve(safeJsonParse(xhr.responseText, null));
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user