CSRF protection for project and recon commands

This commit is contained in:
Antonin Delpeuch 2019-10-14 14:04:38 +01:00
parent a340c137d0
commit 3559eeb11f
31 changed files with 256 additions and 47 deletions

View File

@ -240,6 +240,21 @@ public abstract class Command {
throw new ServletException("Can't find CSRF token: missing or bad URL parameter");
}
/**
* Checks the validity of a CSRF token, without reading the whole POST body.
* Useful when we need to control how the POST body is read (for instance if it
* contains files).
*/
protected boolean hasValidCSRFTokenAsGET(HttpServletRequest request) {
if (request == null) {
throw new IllegalArgumentException("parameter 'request' should not be null");
}
Properties options = ParsingUtilities.parseUrlParameters(request);
String token = options.getProperty("csrf_token");
return token != null && csrfFactory.validToken(token);
}
protected static class HistoryEntryResponse {
@JsonProperty("code")
protected String getCode() { return "ok"; }

View File

@ -56,7 +56,7 @@ public class ImportingControllerCommand extends Command {
@Override
public void doPost(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
if(!checkCSRF(request)) {
if(!hasValidCSRFTokenAsGET(request)) {
respondCSRFError(response);
return;
}
@ -96,14 +96,4 @@ public class ImportingControllerCommand extends Command {
}
return null;
}
/**
* Checks the validity of a CSRF token, without reading the whole POST body.
* See above for details.
*/
private boolean checkCSRF(HttpServletRequest request) {
Properties options = ParsingUtilities.parseUrlParameters(request);
String token = options.getProperty("csrf_token");
return token != null && csrfFactory.validToken(token);
}
}

View File

@ -64,6 +64,10 @@ public class CreateProjectCommand extends Command {
@Override
public void doPost(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
if(!hasValidCSRFTokenAsGET(request)) {
respondCSRFError(response);
return;
}
ProjectManager.singleton.setBusy(true);
try {

View File

@ -49,6 +49,11 @@ public class DeleteProjectCommand extends Command {
@Override
public void doPost(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
if(!hasValidCSRFToken(request)) {
respondCSRFError(response);
return;
}
response.setHeader("Content-Type", "application/json");
try {
long projectID = Long.parseLong(request.getParameter("project"));

View File

@ -46,6 +46,10 @@ import com.google.refine.io.FileProjectManager;
import com.google.refine.model.Project;
public class ExportProjectCommand extends Command {
/**
* This command uses POST but is left CSRF-unprotected as it does not incur a state change.
*/
@Override
public void doPost(HttpServletRequest request, HttpServletResponse response)

View File

@ -61,6 +61,10 @@ import com.google.refine.model.Project;
public class ExportRowsCommand extends Command {
private static final Logger logger = LoggerFactory.getLogger("ExportRowsCommand");
/**
* This command uses POST but is left CSRF-unprotected as it does not incur a state change.
*/
@SuppressWarnings("unchecked")
static public Properties getRequestParameters(HttpServletRequest request) {

View File

@ -55,6 +55,11 @@ import com.google.refine.model.Project;
import com.google.refine.model.RecordModel;
public class GetModelsCommand extends Command {
/**
* This command uses POST but is left CSRF-unprotected as it does not incur a state change.
*/
@Override
public void doPost(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {

View File

@ -63,6 +63,10 @@ public class ImportProjectCommand extends Command {
@Override
public void doPost(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
if(!hasValidCSRFTokenAsGET(request)) {
respondCSRFError(response);
return;
}
ProjectManager.singleton.setBusy(true);
try {

View File

@ -46,7 +46,11 @@ public class RenameProjectCommand extends Command {
@Override
public void doPost(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
if(!hasValidCSRFToken(request)) {
respondCSRFError(response);
return;
}
try {
String name = request.getParameter("name");
ProjectMetadata pm = getProjectMetadata(request);

View File

@ -41,6 +41,10 @@ public class SetProjectMetadataCommand extends Command {
@Override
public void doPost(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
if(!hasValidCSRFToken(request)) {
respondCSRFError(response);
return;
}
Project project = request.getParameter("project") != null ? getProject(request) : null;
String metaName = request.getParameter("name");

View File

@ -43,6 +43,11 @@ public class SetProjectTagsCommand extends Command {
@Override
public void doPost(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
if(!hasValidCSRFToken(request)) {
respondCSRFError(response);
return;
}
response.setHeader("Content-Type", "application/json");
Project project;

View File

@ -93,6 +93,10 @@ public class GuessTypesOfColumnCommand extends Command {
@Override
public void doPost(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
if(!hasValidCSRFToken(request)) {
respondCSRFError(response);
return;
}
try {
Project project = getProject(request);

View File

@ -79,6 +79,10 @@ public class PreviewExtendDataCommand extends Command {
@Override
public void doPost(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
if(!hasValidCSRFToken(request)) {
respondCSRFError(response);
return;
}
try {
Project project = getProject(request);

View File

@ -75,6 +75,10 @@ public class ReconClearOneCellCommand extends Command {
@Override
public void doPost(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
if(!hasValidCSRFToken(request)) {
respondCSRFError(response);
return;
}
try {
Project project = getProject(request);

View File

@ -59,6 +59,10 @@ public class ReconJudgeOneCellCommand extends Command {
@Override
public void doPost(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
if(!hasValidCSRFToken(request)) {
respondCSRFError(response);
return;
}
try {
request.setCharacterEncoding("UTF-8");

View File

@ -0,0 +1,24 @@
package com.google.refine.commands.project;
import com.google.refine.commands.CommandTestBase;
import java.io.IOException;
import javax.servlet.ServletException;
import org.testng.annotations.BeforeMethod;
import org.testng.annotations.Test;
public class ImportProjectCommandTests extends CommandTestBase {
@BeforeMethod
public void setUpCommand() {
command = new ImportProjectCommand();
}
@Test
public void testCSRFProtection() throws ServletException, IOException {
command.doPost(request, response);
assertCSRFCheckFailed();
}
}

View File

@ -0,0 +1,23 @@
package com.google.refine.commands.project;
import com.google.refine.commands.CommandTestBase;
import java.io.IOException;
import javax.servlet.ServletException;
import org.testng.annotations.BeforeMethod;
import org.testng.annotations.Test;
public class RenameProjectCommandTests extends CommandTestBase {
@BeforeMethod
public void setUpCommand() {
command = new RenameProjectCommand();
}
@Test
public void testCSRFProtection() throws ServletException, IOException {
command.doPost(request, response);
assertCSRFCheckFailed();
}
}

View File

@ -58,6 +58,7 @@ import com.fasterxml.jackson.databind.node.ObjectNode;
import com.google.refine.ProjectManager;
import com.google.refine.ProjectMetadata;
import com.google.refine.RefineTest;
import com.google.refine.commands.Command;
import com.google.refine.commands.project.SetProjectMetadataCommand;
import com.google.refine.model.Project;
import com.google.refine.util.ParsingUtilities;
@ -101,6 +102,7 @@ public class SetProjectMetadataCommandTests extends RefineTest {
// mock dependencies
when(request.getParameter("project")).thenReturn(PROJECT_ID);
when(request.getParameter("csrf_token")).thenReturn(Command.csrfFactory.getFreshToken());
when(projMan.getProject(anyLong())).thenReturn(proj);
when(proj.getMetadata()).thenReturn(metadata);

View File

@ -0,0 +1,23 @@
package com.google.refine.commands.project;
import com.google.refine.commands.CommandTestBase;
import java.io.IOException;
import javax.servlet.ServletException;
import org.testng.annotations.BeforeMethod;
import org.testng.annotations.Test;
public class SetProjectTagsCommandTests extends CommandTestBase {
@BeforeMethod
public void setUpCommand() {
command = new SetProjectTagsCommand();
}
@Test
public void testCSRFProtection() throws ServletException, IOException {
command.doPost(request, response);
assertCSRFCheckFailed();
}
}

View File

@ -0,0 +1,23 @@
package com.google.refine.commands.recon;
import com.google.refine.commands.CommandTestBase;
import java.io.IOException;
import javax.servlet.ServletException;
import org.testng.annotations.BeforeMethod;
import org.testng.annotations.Test;
public class GuessTypesOfColumnCommandTests extends CommandTestBase {
@BeforeMethod
public void setUpCommand() {
command = new GuessTypesOfColumnCommand();
}
@Test
public void testCSRFProtection() throws ServletException, IOException {
command.doPost(request, response);
assertCSRFCheckFailed();
}
}

View File

@ -0,0 +1,23 @@
package com.google.refine.commands.recon;
import com.google.refine.commands.CommandTestBase;
import java.io.IOException;
import javax.servlet.ServletException;
import org.testng.annotations.BeforeMethod;
import org.testng.annotations.Test;
public class PreviewExtendDataCommandTests extends CommandTestBase {
@BeforeMethod
public void setUpCommand() {
command = new PreviewExtendDataCommand();
}
@Test
public void testCSRFProtection() throws ServletException, IOException {
command.doPost(request, response);
assertCSRFCheckFailed();
}
}

View File

@ -0,0 +1,24 @@
package com.google.refine.commands.recon;
import com.google.refine.commands.CommandTestBase;
import java.io.IOException;
import javax.servlet.ServletException;
import org.testng.annotations.BeforeMethod;
import org.testng.annotations.Test;
public class ReconClearOneCellCommandTests extends CommandTestBase {
@BeforeMethod
public void setUpCommand() {
command = new ReconClearOneCellCommand();
}
@Test
public void testCSRFProtection() throws ServletException, IOException {
command.doPost(request, response);
assertCSRFCheckFailed();
}
}

View File

@ -82,6 +82,7 @@ public class ReconJudgeOneCellCommandTest extends RefineTest {
response = mock(HttpServletResponse.class);
when(request.getParameter("project")).thenReturn(String.valueOf(project.id));
when(request.getParameter("csrf_token")).thenReturn(Command.csrfFactory.getFreshToken());
writer = mock(PrintWriter.class);
try {

View File

@ -69,7 +69,7 @@ function registerCommands() {
RS.registerCommand(module, "get-project-metadata", new Packages.com.google.refine.commands.project.GetProjectMetadataCommand());
RS.registerCommand(module, "get-all-project-metadata", new Packages.com.google.refine.commands.workspace.GetAllProjectMetadataCommand());
RS.registerCommand(module, "set-metaData", new Packages.com.google.refine.commands.project.SetProjectMetadataCommand());
RS.registerCommand(module, "set-project-metadata", new Packages.com.google.refine.commands.project.SetProjectMetadataCommand());
RS.registerCommand(module, "get-all-project-tags", new Packages.com.google.refine.commands.workspace.GetAllProjectTagsCommand());
RS.registerCommand(module, "set-project-tags", new Packages.com.google.refine.commands.project.SetProjectTagsCommand());

View File

@ -185,7 +185,7 @@ ExtendReconciledDataPreviewDialog.prototype._update = function() {
this._elmts.previewContainer.empty();
} else {
// otherwise, refresh the preview
$.post(
Refine.postCSRF(
"command/core/preview-extend-data?" + $.param(params),
{
rowIndices: JSON.stringify(this._rowIndices),
@ -194,10 +194,10 @@ ExtendReconciledDataPreviewDialog.prototype._update = function() {
function(data) {
self._renderPreview(data);
},
"json"
).fail(function(data) {
alert($.i18n('core-views/internal-err'));
});
"json",
function(data) {
alert($.i18n('core-views/internal-err'));
});
}
};

View File

@ -53,11 +53,14 @@ Refine.wrapCSRF = function(onCSRF) {
// Performs a POST request where an additional CSRF token
// is supplied in the POST data. The arguments match those
// of $.post().
Refine.postCSRF = function(url, data, success, dataType) {
Refine.wrapCSRF(function(token) {
Refine.postCSRF = function(url, data, success, dataType, failCallback) {
return Refine.wrapCSRF(function(token) {
var fullData = data || {};
fullData['csrf_token'] = token;
$.post(url, fullData, success, dataType);
var req = $.post(url, fullData, success, dataType);
if (failCallback !== undefined) {
req.fail(failCallback);
}
});
};

View File

@ -31,16 +31,16 @@ function EditMetadataDialog(metaData, targetRowElem) {
if (newTags !== null) {
$(td1).text(newTags);
metaData[key] = newTags;
$.ajax({
type : "POST",
url : "command/core/set-project-tags",
data : {
Refine.postCSRF(
"command/core/set-project-tags",
{
"project" : project,
"old" : oldTags,
"new" : newTags
},
dataType : "json",
});
function(data) {},
"json"
);
}
Refine.OpenProjectUI.refreshProject(targetRowElem, metaData, project);
@ -58,8 +58,8 @@ function EditMetadataDialog(metaData, targetRowElem) {
if (newValue !== null) {
$(td1).text(newValue);
metaData[key] = newValue;
$.post(
"command/core/set-metaData",
Refine.postCSRF(
"command/core/set-project-metadata",
{
project : project,
name : key,

View File

@ -33,6 +33,10 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
Refine.ImportProjectUI = function(elmt) {
elmt.html(DOM.loadHTML("core", "scripts/index/import-project-ui.html"));
Refine.wrapCSRF(function(token) {
elem.attr('action', "command/core/import-project?" + $.param({ csrf_token: token});
});
this._elmt = elmt;
this._elmts = DOM.bind(elmt);

View File

@ -221,18 +221,17 @@ Refine.OpenProjectUI.prototype._renderProjects = function(data) {
.html("<img src='images/close.png' />")
.click(function() {
if (window.confirm($.i18n('core-index-open/del-body') + project.name + "\"?")) {
$.ajax({
type: "POST",
url: "command/core/delete-project",
data: { "project" : project.id },
dataType: "json",
success: function (data) {
Refine.postCSRF(
"command/core/delete-project",
{ "project" : project.id },
function (data) {
if (data && typeof data.code != 'undefined' && data.code == "ok") {
Refine.TagsManager.allProjectTags = [];
self._buildTagsAndFetchProjects();
}
}
});
},
"json"
);
}
return false;
}).appendTo(

View File

@ -216,20 +216,19 @@ Refine._renameProject = function() {
return;
}
$.ajax({
type: "POST",
url: "command/core/rename-project",
data: { "project" : theProject.id, "name" : name },
dataType: "json",
success: function (data) {
Refine.postCSRF(
"command/core/rename-project",
{ "project" : theProject.id, "name" : name },
function (data) {
if (data && typeof data.code != "undefined" && data.code == "ok") {
theProject.metadata.name = name;
Refine.setTitle();
} else {
alert($.i18n('core-index/error-rename')+" " + data.message);
}
}
});
},
"json"
);
};
/*

View File

@ -44,7 +44,7 @@ ReconStandardServicePanel.prototype._guessTypes = function(f) {
var self = this;
var dismissBusy = DialogSystem.showBusy();
$.post(
Refine.postCSRF(
"command/core/guess-types-of-column?" + $.param({
project: theProject.id,
columnName: this._column.name,
@ -74,7 +74,8 @@ ReconStandardServicePanel.prototype._guessTypes = function(f) {
dismissBusy();
f();
}
},
"json"
);
};