本卡屬 FR-109(母卡見建卡後補號),建議第 1 棒:填答那半——任務問卷、填答/patch/checkpoint、歷史與還原、答案 Excel 匯入、socketio 即時協作、以及 2026-07 補的守門 helper 全部呼叫點。31 檔,只掃不修。
稽核任務指派某人填問卷後:他填、可以存檢查點、可以看歷史版本並還原、可以從 Excel 匯入答案、多人同時填時畫面即時同步、管理者可以設定這份問卷要不要審核。這棒驗:「只有被指派的人或專案管理者能動」這條 2026-07 補的規則,是不是每一條路都走過它;讀答案要不要守;即時同步的房間誰都能進嗎。
這是 FR-048 補洞的對象,總表 §0.5 排這支的理由就是「驗補上去的檢查是否涵蓋所有入口」。守門全在 service 層(route 只有登入),所以要從 32 條 route 逐支追到 service 看有沒有經過 assert_task_survey_writer/_manager。
app/common/task_survey_guard.py;呼叫點 question_answer_service.py:110/:244/:395、question_answer_history_service.py:53、task_survey_service.py:89/:273/:569)。填答側 route 共 13 條(task_survey_route 5、question_answer_route 3、question_answer_history_route 3、history_detail 1、外加 TaskSurveyConfiguredRoute)——逐條追到 service,列表:哪條經過 writer、哪條經過 manager、哪條兩者都沒有。首腦初看:TaskSurveyRoute.get、TaskSurveysAnswersRoute.get(讀答案)、QuestionAnswerHistoryMenuRoute.get、QuestionAnswerHistoriesRoute.post、QuestionAnswerHistoryDetailsRoute.post、TaskSurveysRoute.post(清單)讀取類全部沒有守門——任何登入者拿 task_survey uid 就能讀別人的填答與歷史。這是本棒最大嫌疑,與跨 arc 總表 🅰 組同型。task_survey_guard.py)。_resolve_project_id_by_task 用 task_assignees 任一列的 project_id——task 沒有任何指派列時回 None→manager 分支跳過→writer 只剩「assignee 本人」一條,若也查無就 403(fail-closed,正確);但 task_assignee_domain_service is None(宿主沒注入)時兩個分支都跳過→直接到 raise——確認是 raise 不是 return。participant_role_service.get_user_role 回的 role 比對 ParticipantRole.MANAGER——viewer/member 的語意與總表第 59 項一致嗎。app/handler/fill_survey_socketio_handler.py:49-105)。on_connect/on_join(room):room=task_survey_uid,on_join 用 room 取 get_answer_list_by_task_survey_uid 廣播快照——有沒有驗這個 socket 的使用者是該 task 的 assignee/manager;連線時 JWT 怎麼驗(on_connect(data));Redis fill-survey:<room>:users 的 presence 誰都能寫;emit('reload', ..., room=...) 由 REST 端點觸發(question_answer_history_route.py:57)。task_survey_route.py:128-166 ProjectTaskSurveyAnswerUploadRoute)。secure_filename 已做;load_workbook 有沒有 read_only/大小上限(解析炸彈);匯入後寫答案走的是 update_task_survey_answer(有 writer 守門)還是別的路;TaskSurveyImportTemplateRoute.get(task_uid) :57-127 產範本時會不會把該任務現有答案帶進去(等於讀答案沒守)。question_answer_history_service.py:49-110 revert_question_answer_from_history)。有 writer 守門;但 history_uid 與 task_survey_uid 是分開傳的——history 屬不屬於這份 task_survey 有沒有比對(拿 A 的 history_uid 還原到 B 上)。task_survey_service.py:89/:273/:569 assert_task_survey_manager)。update_task_survey_configure(uid, approver_required, survey_uid, user)——survey_uid 可換,能不能把別租戶的問卷換進來(RLS 靠 join 反查,task_surveys 無租戶欄);_reconcile_task_surveys/_replace_survey_snapshots(宿主 survey_handler.py)走什麼身分。question_answers policy 是 EXISTS(SELECT 1 FROM survey_questions q JOIN survey_pages p ON p.id=q.page_id WHERE q.id=question_answers.question_id)——這條只驗「題目存在且掛在某頁」,沒有再往上到 surveys.tenant_id(首腦截到的 policy 文字看不到 tenant 條件)。要驗:survey_pages/survey_questions 自己的 policy 有沒有一路 join 到 surveys.tenant_id;若中間某層斷了,整條鏈等於「有列就放行」。實查方法:\d+ 或 pg_policies 抓完整 qual 逐層比對。這是本棒第二大嫌疑。app/task_type_declaration.py、app/common/task_survey_status_transition.py 屬 V1 但這棒讀呼叫端)。checkpoint 的 status 由 payload 來(question_answer_route.py:84)——能不能跳狀態、能不能把已審核的打回。app/common/task_survey_guard.py 檔頭自陳「填答與管理端點原本只有 route 層 @jwt_required,service 完全無身分檢查——任何登入者可改任何人的填答」,D10 定調後補了 assert_task_survey_writer(填答=被指派者本人或專案 manager)與 assert_task_survey_manager(管理=專案 manager)。本系列主要目的是驗補上去的洞有沒有真的堵住、有沒有漏掉入口。.3a 問卷 12 張裸表隔離 已 Done);只有 surveys/survey_folders 有 tenant_id 欄,其餘 13 張靠 policy 內 EXISTS(... JOIN ...) 反查到有租戶欄的表——這種「靠 join 反查」的 policy 是本系列要驗的重點之一(join 鏈斷了就變全開或全關)。compliance.job_execution_surveys 零隔離但屬 flow-control。4 顆能力點 survey.* 全客戶層。surveys 表資料量與 question_answers 見卡內數字。api/guards.py 檔頭自陳它是 21 支裡唯一用模組級全域存接線的(process 級非 app 級,同行程兩個 app 第二次 configure() 覆寫第一次),_assert_wiring 缺接線拒絕掛載。確認即可,不當漏洞報。core/plugins/survey.py:178 mount_api=not host.enable_socketio),但 socketio 的 fill-survey namespace handler 在套件內(app/handler/fill_survey_socketio_handler.py)——它的房間加入有沒有驗身分是 V2 重點。