Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adding CSRF support #1967

Merged
merged 3 commits into from
May 22, 2017
Merged

Adding CSRF support #1967

merged 3 commits into from
May 22, 2017

Conversation

Delawen
Copy link
Contributor

@Delawen Delawen commented Apr 27, 2017

This fixes a security bug on https://en.wikipedia.org/wiki/Cross-site_request_forgery

So now, if you are logged in on GeoNetwork and an external third party tries to ajax call or redirect you to any endpoint in GeoNetwork, GeoNetwork will reject the call because it won't contain this extra security token. This token is generated on the server by session and stored on a cookie called XSRF-TOKEN on the client.

By default, all Angular calls will add the csrf as header X-XSRF-TOKEN and/or parameter _csrf containing the security token. All current forms that don't use Angular have also been modified so we don't have problems. The multi-part forms for uploading files added this parameter inside the "action" element of the form.

captura de pantalla de 2017-04-27 13-19-15

Changes on future developments:

  • All requests (POST, PUT, DELETE) should contain this header or parameter. If it is Angular, Angular will take care of it. If it is a direct redirect, call or form submission, make sure you add it.
  • All third party software will have to store the cookie called XSRF-TOKEN after login, so it can add the header or parameter to all calls.

If not, GeoNetwork will return an error asking for this token:
captura de pantalla de 2017-04-27 12-49-57

As a good practice, even GET requests should contain this csrf token, but right now GeoNetwork will skip the check on those calls (default Spring configuration, based on the idea that all GET calls don't modify the platform).

Extra: As Spring -Security 3.2 (security library we are using) does not contain the Cookie filter for Csrf, I just ported it from Spring 4. It is a simple code that works. When (if?) we upgrade, we can remove this file and use the native Spring cookie filter.

@Delawen
Copy link
Contributor Author

Delawen commented Apr 27, 2017

Spring suggest to not use the cookie to store the token as it may depend on the security of the cookie. So it depends on the security of the browser.

But that complicates the code a lot because that means we have to manually add it to any page as a javascript variable to be used inside each call. A lot of workaround and even in the Spring doc they recommend to use the cookie approach for AngularJS explicitly.

@Delawen
Copy link
Contributor Author

Delawen commented Apr 27, 2017

Should we also stop GET calls as they may contain sensible data? Maybe on a future enhancement? Maybe as an optional setting?

Previous Scenario:

  • A malicious webpage can redirect a GET to GN to expose data
  • A malicious webpage can redirect a POST to GN to update existing data
  • A malicious webpage can redirect a PUT to GN to insert new data
  • A malicious webpage can redirect a DELETE to GN to remove existing data

After PR Scenario:

  • A malicious webpage can still redirect a GET to GN to expose data
  • A malicious webpage can NOT redirect a POST to GN to update existing data
  • A malicious webpage can NOT redirect a PUT to GN to insert new data
  • A malicious webpage can NOT redirect a DELETE to GN to remove existing data

@Delawen
Copy link
Contributor Author

Delawen commented Apr 27, 2017

Working on adding the csrf token to the doc api: swagger-api/swagger-ui#1380

Copy link
Member

@josegar74 josegar74 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code looks fine.

Not sure where, but would be good once merged to add documentation about new forms development including the csrf token, either in https://github.com/geonetwork/core-geonetwork/tree/develop/software_development or in http://geonetwork-opensource.org/manuals/trunk/eng/users/customizing-application/index.html

@Delawen
Copy link
Contributor Author

Delawen commented Apr 27, 2017

@fxprunayre I am trying to add the csrf header to the swagger doc but I cannot make it work.

I added it to SwaggerConfig.java as an ApiKey on the array of securitySchemes:

        .securitySchemes(newArrayList(new BasicAuth("basicAuth"), 
            new ApiKey("csrf", "X-XSRF-TOKEN", "")));

and defined it on index.html as:

  window.swaggerUi = new SwaggerUi({
    url: url,
    dom_id: "swagger-ui-container",
    authorizations: {
      csrf: function() {
        // This function will get called /before/ each request
        // ... UNLESS you have a 'security' tag in the swagger.json file, in which case you must add 'csrf' to the list of auths.
        var cookiearray = document.cookie.split(';');
        for(var i =0 ; i < cookiearray.length ; ++i){ 
            if(cookiearray[i].trim().match('^XSRF-TOKEN=')){ 
              this.headers['X-XSRF-TOKEN'] = 
                cookiearray[i].replace(`XSRF-TOKEN=`,'').trim();
            }
        }
        return true; // there is a bug, fixed but not in develop_2.0 where returning true will only process _one_ interceptor, you can leave this if it's your only interceptor and when the fix is in, it'll work as expected
      }
    }, 

It gets applied on the GET requests, but not on the rest (which are the ones that really need it 😅 )

@Delawen
Copy link
Contributor Author

Delawen commented May 2, 2017

Finally I added it as an interceptor before each call. The ApiKey authorization object didn't work for some unknown reason.

So, please check and approve if it is good enough.

Delawen added a commit to geonetwork/doc that referenced this pull request May 2, 2017
@fxprunayre
Copy link
Member

Any example when using CURL for example ? You need to first make a request, get the header and then set it in next calls ?

@Delawen
Copy link
Contributor Author

Delawen commented May 17, 2017

You can still use basic authentication:

$ curl http://localhost:8080/geonetwork/srv/eng/info?type=me --user admin:admin
<?xml version="1.0" encoding="UTF-8"?>
<info>
  <me authenticated="true">
    <profile>Administrator</profile>
    <username>admin</username>
    <id>1</id>
    <name>admin</name>
    <surname>admin</surname>
    <email />
    <organisation />
    <hash />
  </me>
  <env>
    <baseURL>/geonetwork</baseURL>
    <node>srv</node>
  </env>
</info>

Edit: OK, this is a GET example. Let me get back with a POST example.

@Delawen
Copy link
Contributor Author

Delawen commented May 17, 2017

Yes, you need to do first a call to a POST endpoint to get the session and the csrf and then, using the cookie and the token, you can do whatever you want:

$ rm -f /tmp/cookie; curl -c /tmp/cookie http://localhost:8080/geonetwork/srv/eng/info?type=me -X POST ; cat /tmp/cookie

<html>
<head>
<meta http-equiv="Content-Type" content="text/html;charset=utf-8"/>
<title>Error 403 Expected CSRF token not found. Has your session expired?</title>
</head>
<body><h2>HTTP ERROR 403</h2>
<p>Problem accessing /geonetwork/srv/eng/info. Reason:
<pre>    Expected CSRF token not found. Has your session expired?</pre></p><hr><a href="http://eclipse.org/jetty">Powered by Jetty:// 9.3.9.v20160517</a><hr/>

</body>
</html>
# Netscape HTTP Cookie File
# https://curl.haxx.se/docs/http-cookies.html
# This file was generated by libcurl! Edit at your own risk.

localhost       FALSE   /geonetwork     FALSE   0       XSRF-TOKEN      3825c5f1-49b3-4c62-bc3a-1916d5872d02

$ curl -v -X POST ```
"http://localhost:8080/geonetwork/srv/eng/info?type=me" -H "X-XSRF-TOKEN: 3825c5f1-49b3-4c62-bc3a-1916d5872d02" --user admin:admin -b /tmp/cookie

*   Trying ::1...
* TCP_NODELAY set
* Connected to localhost (::1) port 8080 (#0)
* Server auth using Basic with user 'admin'
> POST /geonetwork/srv/eng/info?type=me HTTP/1.1
> Host: localhost:8080
> Authorization: Basic YWRtaW46YWRtaW4=
> User-Agent: curl/7.52.1
> Accept: */*
> Cookie: XSRF-TOKEN=3825c5f1-49b3-4c62-bc3a-1916d5872d02
> X-XSRF-TOKEN: 3825c5f1-49b3-4c62-bc3a-1916d5872d02
> 
< HTTP/1.1 200 OK
< Date: Wed, 17 May 2017 08:52:05 GMT
* Added cookie JSESSIONID="1na1v7dpu7d7bcsnelvh0vnvn" for domain localhost, path /geonetwork, expire 0
< Set-Cookie: JSESSIONID=1na1v7dpu7d7bcsnelvh0vnvn;Path=/geonetwork
< Expires: Thu, 01 Jan 1970 00:00:00 GMT
* Added cookie serverTime="1495011125647" for domain localhost, path /, expire 0
< Set-Cookie: serverTime=1495011125647;Path=/
* Added cookie sessionExpiry="1495011125647" for domain localhost, path /, expire 0
< Set-Cookie: sessionExpiry=1495011125647;Path=/
< Content-Type: application/xml; charset=UTF-8
< Pragma: no-cache
< Cache-Control: no-cache
< Expires: -1
< Transfer-Encoding: chunked
< Server: Jetty(9.3.9.v20160517)
< 
<?xml version="1.0" encoding="UTF-8"?>
<info>
  <me authenticated="true">
    <profile>Administrator</profile>
    <username>admin</username>
    <id>1</id>
    <name>admin</name>
    <surname>admin</surname>
    <email />
    <organisation />
    <hash />
  </me>
  <env>
    <baseURL>/geonetwork</baseURL>
    <node>srv</node>
  </env>
</info>

* Curl_http_done: called premature == 0
* Connection #0 to host localhost left intact

Delawen added a commit to geonetwork/doc that referenced this pull request May 17, 2017
Copy link
Member

@fxprunayre fxprunayre left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not a CSRF expert, but it looks good to me.

@fxprunayre fxprunayre added this to the 3.2.2 milestone May 17, 2017
@Delawen Delawen merged commit b906716 into 3.2.x May 22, 2017
@Delawen Delawen deleted the csrf branch May 22, 2017 09:47
@fxprunayre
Copy link
Member

What should we do when having :

Problem accessing /geonetwork/signin. Reason:

    Invalid CSRF Token '' was found on the request parameter '_csrf' or header 'X-XSRF-TOKEN'.

?

It sounds related to WRO4J - Does it mean that all users moving from 3.2.1 to 3.2.2 will have to manually drop WRO4J db to be able to login ?

@Delawen
Copy link
Contributor Author

Delawen commented Jun 14, 2017 via email

@fxprunayre
Copy link
Member

Is it not renewed on startup?

I'm not 100% sure WRO is doing something on startup to check if it needs to rebuild the cache or not. Anyone ? It could check if there is file change or at least we could maybe trigger a build if version changed (when db migration is triggered maybe ?).

@josegar74
Copy link
Member

I have tested and the wro4j cache file seem updated, at least the timestamp changes. But the problem is there, you need to remove the files manually and restart GeoNetwork, what is not nice.

From 3.2.1:

3633152 Jun 20 10:49 wro4j-cache.h2.db

Migrated to 3.2.2, using the same external folder as in 3.2.1:

5730304 Jun 20 10:58 wro4j-cache.h2.db

Related code to fill the wro4j cache in startup: https://github.com/geonetwork/core-geonetwork/blob/3.2.x/web/src/main/java/org/fao/geonet/Geonetwork.java#L430-L444

Not sure what is done in that code to update the cache, but seem not enough.

Maybe an option is to search for the wro4j-cache-* files before filling the cache and remove them to ensure are recreated. This will slow down a bit the startup, but not sure if even with the JPA changes we can get access to check properly if it's an upgrade or not. Any proposal about this is welcome.

@josegar74
Copy link
Member

The database migration is executed before the Geonetwork.start, where is set the data directory, so not an option seem to cleanup the wro4j files. And in GeoNetwork.startseem not possible to know if a migration has been done as the database already has the current version information.

I have tested the following code in Geonetwork.start:

 // Remove wro4j cache files
 Path wro4jcacheFolder = Paths.get(systemDataDir);
 Files.delete(wro4jcacheFolder.resolve("wro4j-cache.h2.db"));
 Files.delete(wro4jcacheFolder.resolve("wro4j-cache.lock.db"));

 fillCaches(context);

Seems working fine to solve this issue, but has some issues:

  • Will be done on every startup.
  • Should be a better way using probably DiskbackedCache instance, but not very clear how to get this from the current code.

@fxprunayre can you check/test the proposed changes and maybe you have a better idea how to improve this?

@fxprunayre
Copy link
Member

maybe you have a better idea

No better idea.

josegar74 pushed a commit to josegar74/core-geonetwork that referenced this pull request Jun 20, 2017
@josegar74
Copy link
Member

@fxprunayre I found a better way, please if you have time, can you try this PR: #2031 in your system with a migration?

@fxprunayre
Copy link
Member

This breaks the GeoNetwork harvester using authentication right ?

image

This should be handled in here too https://github.com/geonetwork/core-geonetwork/blob/master/harvesters/src/main/java/org/fao/geonet/kernel/harvest/harvester/geonet/Harvester.java#L128 ? Anyone did it already or having examples on how to setup this in Java ?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants