{ "version": "1.1", "id": "ap3-csrf-boundary", "title": "AP3의 masked CSRF 응답과 raw POST credential", "question": "AP3에서 CSRF 응답의 masked token과 raw cookie 중 무엇이 실제 POST header의 data source가 되는가?", "type": "data-flow", "direction": "LR", "audience": [ "Spring BFF의 cookie 기반 CSRF 경계를 구현·검토하는 개발자" ], "summary": "BFF는 raw XSRF cookie와 masked JSON token을 함께 내보내지만, SPA는 raw cookie를 읽어 같은 raw 값을 POST header에 넣고 Spring CSRF filter가 cookie와 header를 비교한다.", "alt": "BFF CSRF endpoint가 raw XSRF cookie와 masked JSON token으로 분기하고, SPA가 raw cookie만 실제 POST header 값으로 사용해 Spring CSRF filter에 제출하는 데이터 흐름.", "long_description": "왼쪽의 BFF CSRF endpoint에서 두 결과가 갈라진다. XSRF-TOKEN cookie에는 raw token이 저장되고 JSON body에는 XOR와 Base64로 masked된 token 및 headerName이 담긴다. 두 결과는 SPA의 POST 조립 단계로 모이지만, JSON에서는 headerName만 사용하고 실제 X-XSRF-TOKEN 값은 document.cookie에서 읽은 raw token이다. POST에는 같은 raw 값을 가진 cookie와 header가 함께 도달하고 Spring CSRF filter가 일치 여부를 확인한다.", "source_context": { "document": "document.md", "document_sha256": "df4d1a604c74e756672b5b40510abfedb8c67b39af280a5f51985ea9972f5371", "anchor": { "kind": "marker", "value": "ap3-csrf-boundary", "line": 858 } }, "composition": { "profile": "component-flow", "diagram_only": true, "reference_ids": [ "payment-event-flow" ], "rationale": "CSRF endpoint의 한 응답이 raw cookie와 masked JSON으로 분기한 뒤 SPA request assembly에서 다시 합쳐져 filter 검증으로 이어지는 명시적 데이터 경로가 중심이므로 component-flow가 적합하다.", "focus_node": "raw-cookie" }, "groups": [], "nodes": [ { "id": "csrf-endpoint", "label": "BFF · /bff/csrf", "kind": "service", "role": "source", "description": "CookieCsrfTokenRepository와 CsrfController를 통해 raw cookie와 masked JSON 응답을 만드는 endpoint.", "evidence": [ { "start_line": 819, "end_line": 844 } ], "assumption": false }, { "id": "raw-cookie", "label": "Browser cookie · raw", "kind": "credential", "role": "store", "shape": "box", "details": [ "XSRF-TOKEN", "JavaScript-readable", "실제 header data source" ], "emphasis": "primary", "description": "Cookie repository가 path /에 설정하며 SPA가 document.cookie로 읽는 raw CSRF token.", "evidence": [ { "start_line": 827, "end_line": 834 }, { "start_line": 844, "end_line": 856 } ], "assumption": false }, { "id": "masked-json", "label": "JSON body · masked", "kind": "data", "role": "store", "shape": "document", "details": [ "token = XOR/Base64", "headerName metadata", "POST token 값으로 미사용" ], "description": "Request attribute용 token을 masked한 JSON 표현으로, SPA는 token 값이 아니라 headerName만 사용한다.", "evidence": [ { "start_line": 836, "end_line": 846 } ], "assumption": false }, { "id": "post-assembler", "label": "SPA POST 조립", "kind": "component", "role": "service", "details": [ "Cookie 자동 첨부", "document.cookie raw → header", "JSON headerName만 사용" ], "description": "Raw cookie 값을 X-XSRF-TOKEN header에 넣어 preference POST를 만드는 browser code.", "evidence": [ { "start_line": 846, "end_line": 856 }, { "start_line": 862, "end_line": 870 } ], "assumption": false }, { "id": "csrf-filter", "label": "Spring CSRF filter", "kind": "security", "role": "sink", "details": [ "raw cookie = raw header 비교", "일치 → controller", "부재·불일치 → 403" ], "description": "Repository의 expected raw token과 submitted raw header를 controller 실행 전에 비교하는 방어선.", "evidence": [ { "start_line": 854, "end_line": 854 }, { "start_line": 873, "end_line": 873 }, { "start_line": 897, "end_line": 904 } ], "assumption": false } ], "edges": [ { "id": "endpoint-to-raw-cookie", "from": "csrf-endpoint", "to": "raw-cookie", "label": "Set-Cookie · raw", "kind": "response", "evidence": [ { "start_line": 827, "end_line": 834 } ], "assumption": false }, { "id": "endpoint-to-masked-json", "from": "csrf-endpoint", "to": "masked-json", "label": "masked JSON", "kind": "response", "evidence": [ { "start_line": 836, "end_line": 844 } ], "assumption": false }, { "id": "raw-cookie-to-assembler", "from": "raw-cookie", "to": "post-assembler", "label": "document.cookie · raw", "kind": "data", "emphasis": "primary", "evidence": [ { "start_line": 846, "end_line": 856 } ], "assumption": false }, { "id": "masked-json-to-assembler", "from": "masked-json", "to": "post-assembler", "label": "headerName only", "kind": "data", "evidence": [ { "start_line": 846, "end_line": 856 } ], "assumption": false }, { "id": "assembler-to-filter", "from": "post-assembler", "to": "csrf-filter", "label": "Cookie raw = Header raw", "kind": "request", "emphasis": "primary", "evidence": [ { "start_line": 848, "end_line": 873 } ], "assumption": false } ], "legend": [], "metadata": { "rationale": "Token masking 자체보다 실제 state-changing request의 credential source와 server comparison을 한 방향의 분기·합류 데이터 흐름으로 보여 준다." } }