feat(12.1-01): add cross-origin nextLink guard and Drive/OneDrive contract tests

- Validate @odata.nextLink hostname against graph.microsoft.com before following
  (T-12.1-03); cross-origin nextLink returns complete=False with prior items retained
- Add TestOneDriveCrossOriginNextLink: cross-origin rejection, same-origin follows,
  page failure retains prior items
- Add TestGoogleDriveAuthFailureControl: 401 returns complete=False, page1+page2
  error retains page1 items
- All 163 four-provider contract tests pass
This commit is contained in:
curo1305
2026-06-22 08:12:39 +02:00
parent 2b46f74329
commit 805fe44bfb
2 changed files with 172 additions and 2 deletions
+14 -2
View File
@@ -28,6 +28,8 @@ import io
import uuid
from typing import Optional
import urllib.parse
import httpx
import msal
@@ -50,6 +52,9 @@ from storage.google_drive_backend import CloudConnectionError # reuse shared ex
GRAPH_BASE = "https://graph.microsoft.com/v1.0"
CHUNK_SIZE = 10 * 1024 * 1024 # 10 MB — above Graph's 4 MB simple upload limit (Pitfall 6)
# Trusted Graph API origin for @odata.nextLink validation (T-12.1-03)
_GRAPH_HOST = "graph.microsoft.com"
class OneDriveBackend(StorageBackend, CloudResourceAdapter):
"""Microsoft Graph / OneDrive implementation of StorageBackend.
@@ -351,7 +356,6 @@ class OneDriveBackend(StorageBackend, CloudResourceAdapter):
content_type: Optional[str] = None
if "file" in item:
content_type = item["file"].get("mimeType")
parent_ref_val = item.get("parentReference", {}).get("id")
resources.append(
CloudResource(
id=uuid.uuid4(),
@@ -367,7 +371,15 @@ class OneDriveBackend(StorageBackend, CloudResourceAdapter):
etag=item.get("eTag") or item.get("cTag"),
)
)
url = data.get("@odata.nextLink")
# Validate @odata.nextLink origin before following (T-12.1-03)
next_link = data.get("@odata.nextLink")
if next_link:
parsed_next = urllib.parse.urlparse(next_link)
if parsed_next.hostname != _GRAPH_HOST:
# Cross-origin continuation URL — untrusted; stop pagination
complete = False
break
url = next_link
except Exception:
complete = False