Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 12 additions & 1 deletion carwatch/webchat.py
Original file line number Diff line number Diff line change
Expand Up @@ -2353,6 +2353,10 @@ def _send(self, code, body, ctype="text/html; charset=utf-8"):
data = body if isinstance(body, bytes) else body.encode()
self.send_response(code)
self.send_header("Content-Type", ctype)
# The dashboard token travels as ?t=<token> so a plain link works in
# the car with one tap (#12). That means every outbound link from a
# dashboard page would otherwise carry the token in its Referer.
self.send_header("Referrer-Policy", "no-referrer")
self.send_header("Content-Length", str(len(data)))
if "html" in ctype:
# The dashboard changed several times in one day; a cached copy
Expand Down Expand Up @@ -2856,7 +2860,14 @@ def run(cmd):
body = json.dumps(payload).encode()
self.send_response(200)
self.send_header("Content-Type", "application/json")
self.send_header("Access-Control-Allow-Origin", "*")
# NO Access-Control-Allow-Origin. This endpoint returns live car
# facts, the Pi's process list and agent journal lines, and on
# home wifi _peer_is_owner() authorises by NETWORK rather than by
# token. Ambient authority plus a wildcard CORS header means any
# web page open on a phone on that wifi could fetch this and read
# the response cross-origin. The dashboard is same-origin and
# never needed the header (#12).
self.send_header("Referrer-Policy", "no-referrer")
self.send_header("Content-Length", str(len(body)))
self.end_headers()
self.wfile.write(body)
Expand Down
54 changes: 54 additions & 0 deletions tests/test_dash_headers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
"""#12: the dashboard token rides in the URL, so headers matter.

Two findings, both about /api/status, which returns live car facts, the Pi's
process list and agent journal lines:

1. It carried `Access-Control-Allow-Origin: *`. On home wifi the dashboard
authorises by NETWORK (_peer_is_owner), not by token - ambient authority.
Ambient authority plus wildcard CORS means any web page open on a phone on
that wifi could fetch this endpoint and read the reply cross-origin.
2. Nothing set Referrer-Policy, so a `?t=<token>` URL leaked the credential in
the Referer of every outbound link.
"""
import os
import re
import sys
import unittest

sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))

SRC = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))),
"carwatch", "webchat.py")


class DashboardHeaders(unittest.TestCase):

def setUp(self):
with open(SRC) as f:
self.src = f.read()

def test_no_wildcard_cors_anywhere(self):
# A comment naming the header is fine; actually sending it is not.
sent = re.findall(r'send_header\(\s*["\']Access-Control-Allow-Origin["\']',
self.src)
self.assertEqual(sent, [], "the dashboard is same-origin and must not "
"advertise CORS on token-gated car data")

def test_referrer_policy_is_sent(self):
self.assertIn('send_header("Referrer-Policy", "no-referrer")', self.src,
"the token travels as ?t=, so it would leak in Referer")

def test_status_endpoint_still_returns_the_dashboard_payload(self):
# Removing a header must not have disturbed the response body.
for key in ('"device"', '"listening"', '"facts"', '"top"', '"journal"'):
self.assertIn(key, self.src)

def test_ambient_auth_still_exists_so_the_cors_fix_matters(self):
# If this ever stops being true the finding changes shape, so assert
# the premise rather than leaving it in a comment.
self.assertIn("_pi_on_home_wifi()", self.src)
self.assertIn("def _peer_is_owner", self.src)


if __name__ == "__main__":
unittest.main()
Loading