CSRF protection for Wikidata extension

This commit is contained in:
Antonin Delpeuch 2019-10-17 10:42:05 +01:00
parent 24feda600a
commit 1ee5068f0d
12 changed files with 67 additions and 8 deletions

View File

@ -56,7 +56,7 @@ ManageAccountDialog.display = function(logged_in_username, saved_credentials, ca
elmts.loginButton.click(function() { elmts.loginButton.click(function() {
frame.hide(); frame.hide();
$.post( Refine.postCSRF(
"command/wikidata/login", "command/wikidata/login",
elmts.loginForm.serialize(), elmts.loginForm.serialize(),
function(data) { function(data) {
@ -71,7 +71,7 @@ ManageAccountDialog.display = function(logged_in_username, saved_credentials, ca
}); });
elmts.logoutButton.click(function() { elmts.logoutButton.click(function() {
$.post( Refine.postCSRF(
"command/wikidata/login", "command/wikidata/login",
"logout=true", "logout=true",
function(data) { function(data) {

View File

@ -94,7 +94,7 @@ PerformEditsDialog.checkAndLaunch = function () {
ManageAccountDialog.ensureLoggedIn(function(logged_in_username) { ManageAccountDialog.ensureLoggedIn(function(logged_in_username) {
if (logged_in_username) { if (logged_in_username) {
var discardWaiter = DialogSystem.showBusy($.i18n('perform-wikidata-edits/analyzing-edits')); var discardWaiter = DialogSystem.showBusy($.i18n('perform-wikidata-edits/analyzing-edits'));
$.post( Refine.postCSRF(
"command/wikidata/preview-wikibase-schema?" + $.param({ project: theProject.id }), "command/wikidata/preview-wikibase-schema?" + $.param({ project: theProject.id }),
{ engine: JSON.stringify(ui.browsingEngine.getJSON()) }, { engine: JSON.stringify(ui.browsingEngine.getJSON()) },
function(data) { function(data) {

View File

@ -1283,7 +1283,7 @@ SchemaAlignmentDialog.preview = function() {
$('.invalid-schema-warning').show(); $('.invalid-schema-warning').show();
return; return;
} }
$.post( Refine.postCSRF(
"command/wikidata/preview-wikibase-schema?" + $.param({ project: theProject.id }), "command/wikidata/preview-wikibase-schema?" + $.param({ project: theProject.id }),
{ schema: JSON.stringify(schema), engine: JSON.stringify(ui.browsingEngine.getJSON()) }, { schema: JSON.stringify(schema), engine: JSON.stringify(ui.browsingEngine.getJSON()) },
function(data) { function(data) {

View File

@ -41,6 +41,11 @@ public class LoginCommand extends Command {
@Override @Override
public void doPost(HttpServletRequest request, HttpServletResponse response) public void doPost(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException { throws ServletException, IOException {
if(!hasValidCSRFToken(request)) {
respondCSRFError(response);
return;
}
String username = request.getParameter("wb-username"); String username = request.getParameter("wb-username");
String password = request.getParameter("wb-password"); String password = request.getParameter("wb-password");
String remember = request.getParameter("remember-credentials"); String remember = request.getParameter("remember-credentials");

View File

@ -46,6 +46,13 @@ import com.google.refine.model.Project;
public class PreviewWikibaseSchemaCommand extends Command { public class PreviewWikibaseSchemaCommand extends Command {
/**
* This command uses POST but is left CSRF-unprotected since it does not
* incur a side effect or state change in the backend.
* The reason why it uses POST is to make sure large schemas and engines
* can be passed as parameters.
*/
@Override @Override
public void doPost(HttpServletRequest request, HttpServletResponse response) public void doPost(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException { throws ServletException, IOException {

View File

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

View File

@ -1,6 +1,7 @@
package org.openrefine.wikidata.commands; package org.openrefine.wikidata.commands;
import static org.testng.Assert.assertEquals; import static org.testng.Assert.assertEquals;
import static org.mockito.Mockito.when;
import java.io.IOException; import java.io.IOException;
@ -9,6 +10,9 @@ import javax.servlet.ServletException;
import org.testng.annotations.BeforeMethod; import org.testng.annotations.BeforeMethod;
import org.testng.annotations.Test; import org.testng.annotations.Test;
import com.google.refine.commands.Command;
import com.google.refine.util.TestUtils;
public class LoginCommandTest extends CommandTest { public class LoginCommandTest extends CommandTest {
@BeforeMethod @BeforeMethod
@ -18,8 +22,16 @@ public class LoginCommandTest extends CommandTest {
@Test @Test
public void testNoCredentials() throws ServletException, IOException { public void testNoCredentials() throws ServletException, IOException {
when(request.getParameter("csrf_token")).thenReturn(Command.csrfFactory.getFreshToken());
command.doPost(request, response); command.doPost(request, response);
assertEquals("{\"logged_in\":false,\"username\":null}", writer.toString()); assertEquals("{\"logged_in\":false,\"username\":null}", writer.toString());
} }
@Test
public void testCsrfProtection() throws ServletException, IOException {
command.doPost(request, response);
TestUtils.assertEqualAsJson("{\"code\":\"error\",\"message\":\"Missing or invalid csrf_token parameter\"}", writer.toString());
}
} }

View File

@ -34,6 +34,9 @@ import javax.servlet.ServletException;
import org.testng.annotations.BeforeMethod; import org.testng.annotations.BeforeMethod;
import org.testng.annotations.Test; import org.testng.annotations.Test;
import com.google.refine.commands.Command;
import com.google.refine.util.TestUtils;
public class SaveWikibaseSchemaCommandTest extends SchemaCommandTest { public class SaveWikibaseSchemaCommandTest extends SchemaCommandTest {
@BeforeMethod @BeforeMethod
@ -44,6 +47,8 @@ public class SaveWikibaseSchemaCommandTest extends SchemaCommandTest {
@Test @Test
public void testValidSchema() public void testValidSchema()
throws ServletException, IOException { throws ServletException, IOException {
when(request.getParameter("csrf_token")).thenReturn(Command.csrfFactory.getFreshToken());
String schemaJson = jsonFromFile("schema/inception.json").toString(); String schemaJson = jsonFromFile("schema/inception.json").toString();
when(request.getParameter("schema")).thenReturn(schemaJson); when(request.getParameter("schema")).thenReturn(schemaJson);
@ -54,6 +59,8 @@ public class SaveWikibaseSchemaCommandTest extends SchemaCommandTest {
@Test @Test
public void testInvalidSchema() throws ServletException, IOException { public void testInvalidSchema() throws ServletException, IOException {
when(request.getParameter("csrf_token")).thenReturn(Command.csrfFactory.getFreshToken());
String schemaJson = "{\"itemDocuments\":[{\"statementGroups\":[{\"statements\":[]}]," String schemaJson = "{\"itemDocuments\":[{\"statementGroups\":[{\"statements\":[]}],"
+"\"nameDescs\":[]}],\"wikibasePrefix\":\"http://www.wikidata.org/entity/\"}"; +"\"nameDescs\":[]}],\"wikibasePrefix\":\"http://www.wikidata.org/entity/\"}";
@ -62,4 +69,13 @@ public class SaveWikibaseSchemaCommandTest extends SchemaCommandTest {
assertTrue(writer.toString().contains("\"error\"")); assertTrue(writer.toString().contains("\"error\""));
} }
@Test
public void testCsrfProtection() throws ServletException, IOException {
String schemaJson = jsonFromFile("schema/inception.json").toString();
when(request.getParameter("schema")).thenReturn(schemaJson);
command.doPost(request, response);
TestUtils.assertEqualAsJson("{\"code\":\"error\",\"message\":\"Missing or invalid csrf_token parameter\"}", writer.toString());
}
} }

View File

@ -32,6 +32,7 @@ import javax.servlet.ServletException;
import org.testng.annotations.Test; import org.testng.annotations.Test;
import com.google.refine.commands.Command;
import com.google.refine.util.ParsingUtilities; import com.google.refine.util.ParsingUtilities;
public abstract class SchemaCommandTest extends CommandTest { public abstract class SchemaCommandTest extends CommandTest {
@ -39,9 +40,11 @@ public abstract class SchemaCommandTest extends CommandTest {
@Test @Test
public void testNoSchema() public void testNoSchema()
throws ServletException, IOException { throws ServletException, IOException {
when(request.getParameter("csrf_token")).thenReturn(Command.csrfFactory.getFreshToken());
command.doPost(request, response); command.doPost(request, response);
assertEquals("{\"code\":\"error\",\"message\":\"No Wikibase schema provided.\"}", writer.toString()); assertEquals(writer.toString(), "{\"code\":\"error\",\"message\":\"No Wikibase schema provided.\"}");
} }
@Test @Test

View File

@ -56,7 +56,11 @@ Refine.wrapCSRF = function(onCSRF) {
Refine.postCSRF = function(url, data, success, dataType, failCallback) { Refine.postCSRF = function(url, data, success, dataType, failCallback) {
return Refine.wrapCSRF(function(token) { return Refine.wrapCSRF(function(token) {
var fullData = data || {}; var fullData = data || {};
fullData['csrf_token'] = token; if (typeof fulldata == 'string') {
fullData = fullData + $.param({csrf_token: token});
} else {
fullData['csrf_token'] = token;
}
var req = $.post(url, fullData, success, dataType); var req = $.post(url, fullData, success, dataType);
if (failCallback !== undefined) { if (failCallback !== undefined) {
req.fail(failCallback); req.fail(failCallback);

View File

@ -55,7 +55,11 @@ Refine.wrapCSRF = function(onCSRF) {
Refine.postCSRF = function(url, data, success, dataType, failCallback) { Refine.postCSRF = function(url, data, success, dataType, failCallback) {
return Refine.wrapCSRF(function(token) { return Refine.wrapCSRF(function(token) {
var fullData = data || {}; var fullData = data || {};
fullData['csrf_token'] = token; if (typeof fulldata == 'string') {
fullData = fullData + $.param({csrf_token: token});
} else {
fullData['csrf_token'] = token;
}
var req = $.post(url, fullData, success, dataType); var req = $.post(url, fullData, success, dataType);
if (failCallback !== undefined) { if (failCallback !== undefined) {
req.fail(failCallback); req.fail(failCallback);

View File

@ -420,7 +420,11 @@ Refine.wrapCSRF = function(onCSRF) {
Refine.postCSRF = function(url, data, success, dataType) { Refine.postCSRF = function(url, data, success, dataType) {
Refine.wrapCSRF(function(token) { Refine.wrapCSRF(function(token) {
var fullData = data || {}; var fullData = data || {};
fullData['csrf_token'] = token; if (typeof fulldata == 'string') {
fullData = fullData + $.param({csrf_token: token});
} else {
fullData['csrf_token'] = token;
}
$.post(url, fullData, success, dataType); $.post(url, fullData, success, dataType);
}); });
}; };