Repository: internetarchive/openlibrary

Issue:
Vulnerability CWE-79 found in patron-metadata/index.js
**Source of Vulnerability**
https://github.com/internetarchive/openlibrary/blob/133ee89ae2bbb290d4a805b2be4763906aa06765/openlibrary/plugins/openlibrary/js/patron-metadata/index.js#L104

https://github.com/internetarchive/openlibrary/blob/133ee89ae2bbb290d4a805b2be4763906aa06765/openlibrary/plugins/openlibrary/js/patron-metadata/index.js#L43

**Vulnerability Description :** Unsanitized input from data from a remote resource flows into append, where it is used to dynamically construct the HTML page on client side. This may result in a DOM Based Cross-Site Scripting attack (DOMXSS). A extended description of the vulnerability can be found [here](https://cwe.mitre.org/data/definitions/79.html)

**Possible Fixes** : Sanitize data input in an HTTP request before reflecting it back, ensuring all data is validated, filtered or escaped before echoing anything back to the user, such as the values of query parameters during searches.


Discussion:
Data used to generate the HTML is coming from Open Library servers.  Is there any reason to treat this as an untrusted source?

Regardless of the answer, I don't think that there is a compelling reason to generate the form on the client side now that observations live in OL servers.  This code can be moved to [UserMetadata.html](https://github.com/internetarchive/openlibrary/blob/master/openlibrary/macros/UserMetadata.html) instead (preferably after #5089 has been merged).

Resolving patch:
diff --git a/openlibrary/core/models.py b/openlibrary/core/models.py
--- a/openlibrary/core/models.py
+++ b/openlibrary/core/models.py
@@ -2,6 +2,7 @@
 """
 import web
 import requests
+from collections import defaultdict
 
 from infogami.infobase import client
 
@@ -13,6 +14,7 @@
 from openlibrary.core.helpers import private_collection_in
 from openlibrary.core.bookshelves import Bookshelves
 from openlibrary.core.booknotes import Booknotes
+from openlibrary.core.observations import Observations
 from openlibrary.core.ratings import Ratings
 from openlibrary.utils.isbn import to_isbn_13, isbn_13_to_isbn_10, canonical
 from openlibrary.core.vendors import create_edition_from_amazon_metadata
@@ -493,6 +495,19 @@ def get_users_notes(self, username, edition_olid=None):
         edition_id = extract_numeric_id_from_olid(edition_olid) if edition_olid else -1
         return Booknotes.get_patron_booknote(username, work_id, edition_id=edition_id)
 
+    def get_users_observations(self, username):
+        if not username:
+            return None
+        work_id = extract_numeric_id_from_olid(self.key)
+        raw_observations = Observations.get_patron_observations(username, work_id)
+        formatted_observations = defaultdict(list)
+
+        for r in raw_observations:
+            kv_pair = Observations.get_key_value_pair(r['type'], r['value'])
+            formatted_observations[kv_pair.key].append(kv_pair.value)
+
+        return formatted_observations
+
     def get_num_users_by_bookshelf(self):
         if not self.key:  # a dummy work
             return {'want-to-read': 0, 'currently-reading': 0, 'already-read': 0}
diff --git a/openlibrary/core/observations.py b/openlibrary/core/observations.py
--- a/openlibrary/core/observations.py
+++ b/openlibrary/core/observations.py
@@ -246,6 +246,7 @@
 cache_duration = config.get('observation_cache_duration') or 86400
 
 
+@public
 @cache.memoize(engine="memcache", key="observations", expires=cache_duration)
 def get_observations():
     """
diff --git a/openlibrary/plugins/openlibrary/api.py b/openlibrary/plugins/openlibrary/api.py
--- a/openlibrary/plugins/openlibrary/api.py
+++ b/openlibrary/plugins/openlibrary/api.py
@@ -141,7 +141,8 @@ def POST(self, work_id):
         """
         user = accounts.get_current_user()
         i = web.input(notes=None, edition_id=None, redir=None)
-        edition_id = int(extract_numeric_id_from_olid(i.edition_id)) if i.edition_id else None
+        edition_id = int(
+            extract_numeric_id_from_olid(i.edition_id)) if i.edition_id else -1
 
         if not user:
             raise web.seeother('/account/login?redirect=/works/%s' % work_id)
@@ -154,7 +155,7 @@ def response(msg, status="success"):
             }), content_type="application/json")
 
         if i.notes is None:
-            Booknotes.remove(username, work_id)
+            Booknotes.remove(username, work_id, edition_id=edition_id)
             return response('removed note')
 
         Booknotes.add(
@@ -437,23 +438,6 @@ class patron_observations(delegate.page):
     path = r"/works/OL(\d+)W/observations"
     encoding = "json"
 
-    def GET(self, work_id):
-        user = accounts.get_current_user()
-
-        if not user:
-            raise web.seeother('/account/login')
-
-        username = user.key.split('/')[2]
-        existing_records = Observations.get_patron_observations(username, work_id)
-
-        patron_observations = defaultdict(list)
-
-        for r in existing_records:
-            kv_pair = Observations.get_key_value_pair(r['type'], r['value'])
-            patron_observations[kv_pair.key].append(kv_pair.value)
-            
-        return delegate.RawText(json.dumps(patron_observations), content_type="application/json")
-
     def POST(self, work_id):
         user = accounts.get_current_user()
 


Test patch: