Skip to content

Commit

Permalink
Change document capture CSRF handling to optional
Browse files Browse the repository at this point in the history
**Why**: As discovered in #5746 (e40b020), if forgery prevention is disabled (e.g. test environment), then there is no CSRF token meta tag on the page. Thus, our assumptions that this would always be present are mistaken. To avoid appending an invalid null token to relevant requests in an environment where forgery protection is disabled, we should properly handle the possibility of its absence.
  • Loading branch information
aduth committed Jan 7, 2022
1 parent 94f7f1e commit 88d0089
Show file tree
Hide file tree
Showing 4 changed files with 15 additions and 13 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ function ButtonTo({ url, method, children, ...buttonProps }) {
{createPortal(
<form ref={formRef} method="post" action={url}>
<input type="hidden" name="_method" value={method} />
<input type="hidden" name="authenticity_token" value={csrf} />
{csrf && <input type="hidden" name="authenticity_token" value={csrf} />}
</form>,
document.body,
)}
Expand Down
6 changes: 3 additions & 3 deletions app/javascript/packages/document-capture/context/upload.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ const UploadContext = createContext({
flowPath: /** @type {FlowPath} */ ('standard'),
startOverURL: /** @type {string} */ (''),
cancelURL: /** @type {string} */ (''),
csrf: /** @type {string} */ (''),
csrf: /** @type {string?} */ (null),
});

UploadContext.displayName = 'UploadContext';
Expand All @@ -34,7 +34,7 @@ UploadContext.displayName = 'UploadContext';
*
* @prop {'POST'|'PUT'} method HTTP method to send payload.
* @prop {string} endpoint Endpoint to which payload should be sent.
* @prop {string} csrf CSRF token to send as parameter to upload implementation.
* @prop {string?} csrf CSRF token to send as parameter to upload implementation.
*/

/**
Expand Down Expand Up @@ -73,7 +73,7 @@ UploadContext.displayName = 'UploadContext';
* @prop {string=} statusEndpoint Endpoint from which to request async upload status.
* @prop {number=} statusPollInterval Interval at which to poll for status, in milliseconds.
* @prop {'POST'|'PUT'} method HTTP method to send payload.
* @prop {string} csrf CSRF token to send as parameter to upload implementation.
* @prop {string?} csrf CSRF token to send as parameter to upload implementation.
* @prop {Record<string,any>=} formData Extra form data to merge into the payload before uploading
* @prop {FlowPath} flowPath The user's session flow path, one of "standard" or "hybrid".
* @prop {string} startOverURL URL to application DELETE path for session restart.
Expand Down
13 changes: 6 additions & 7 deletions app/javascript/packages/document-capture/services/upload.js
Original file line number Diff line number Diff line change
Expand Up @@ -54,13 +54,12 @@ export function toFormEntryError(uploadFieldError) {
* @type {import('../context/upload').UploadImplementation}
*/
async function upload(payload, { method = 'POST', endpoint, csrf }) {
const response = await window.fetch(endpoint, {
method,
headers: {
'X-CSRF-Token': csrf,
},
body: toFormData(payload),
});
/** @type {HeadersInit} */
const headers = {};
if (csrf) {
headers['X-CSRF-Token'] = csrf;
}
const response = await window.fetch(endpoint, { method, headers, body: toFormData(payload) });

if (!response.ok && !response.status.toString().startsWith('4')) {
// 4xx is an expected error state, handled after JSON deserialization. Anything else not OK
Expand Down
7 changes: 5 additions & 2 deletions app/javascript/packs/document-capture.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,7 @@ const noticeError = (error) =>
loadPolyfills(['fetch', 'crypto', 'url']).then(async () => {
const backgroundUploadURLs = getBackgroundUploadURLs();
const isAsyncForm = Object.keys(backgroundUploadURLs).length > 0;
const csrf = /** @type {string} */ (getMetaContent('csrf-token'));
const csrf = getMetaContent('csrf-token');

const formData = {
document_capture_session_uuid: appRoot.getAttribute('data-document-capture-session-uuid'),
Expand All @@ -152,7 +152,10 @@ loadPolyfills(['fetch', 'crypto', 'url']).then(async () => {
}

const keepAlive = () =>
window.fetch(keepAliveEndpoint, { method: 'POST', headers: { 'X-CSRF-Token': csrf } });
window.fetch(keepAliveEndpoint, {
method: 'POST',
headers: /** @type {string[][]} */ ([csrf && ['X-CSRF-Token', csrf]].filter(Boolean)),
});

const {
helpCenterRedirectUrl: helpCenterRedirectURL,
Expand Down

0 comments on commit 88d0089

Please sign in to comment.