시험 제출 전 정답·해설 노출 차단: 서버 서명 권한과 공개 시점 검증

이 글의 목차 7개 항목
  • 분야: Security
  • 기술 스택: TypeScript · Cloudflare Workers · Cloudflare D1 · Web Crypto API

문제와 원인

같은 사용자의 미제출 시험 문항 ID를 일반 풀이 API에 넣고 mode: "practice"를 보내면 정답 조회·풀이 저장·피드백 반환으로 이어졌다. 시험 API의 제출 상태 검사와 일반 풀이 API의 정답 접근이 분리돼, 클라이언트의 풀이 모드만으로 시험 문항의 보호 데이터에 접근할 수 있었다.

수정 전 코드

saveLearningAttempt

const selectedExam = selectedExamType(payload.examType);
const question = await repository.findAttemptQuestion(questionId);

// 문항·선택지·풀이 입력 검증과 채점
const stored = await repository.insertAttemptIdempotent({
  questionId,
  // 풀이 결과와 저장 식별자
});

return Response.json({
  attempt: { /* 저장된 풀이 */ },
  feedback: questionFeedbackPayload(question),
  duplicate: !stored.created,
}, { status: stored.created ? 201 : 200 });

수정 후 코드

saveLearningAttempt

HMAC-SHA256 권한을 사용자·SQL/SW 엔진·문항·학습 문맥에 묶고 정답 조회 앞에서 검증한다. 다른 사용자·문항·엔진·세션으로 바꾸거나 만료된 권한을 사용하면 거부한다.

const selectedExam = selectedExamType(payload.examType);
await requirePracticeFeedbackAccess({
  key,
  questionId,
  feedbackAuthorization: payload.feedbackAuthorization,
  repository,
});
const question = await repository.findAttemptQuestion(questionId);

feedbackRevealDecision

유효한 연습 권한이 있어도 같은 사용자의 시험에 포함된 문항이 submitted 이전이면 HTTP 403으로 종료한다. 알 수 없는 시험 상태도 이 조건에 포함된다.

if (input?.activity === "practice") {
  if (session?.ownerMatches === true
    && session?.containsQuestion === true
    && session.status !== "submitted") {
    return { allowed: false, reason: "EXAM_FEEDBACK_NOT_RELEASED" };
  }
  return input?.authorizationValid === true
    ? { allowed: true, reason: "PRACTICE_FEEDBACK_AUTHORIZED" }
    : { allowed: false, reason: "PRACTICE_FEEDBACK_AUTHORIZATION_REQUIRED" };
}

문항 전달 단계에서도 미제출 시험 문항의 연습 권한 발급을 막고 공개 문항·보기만 선택한다. SW 연습 권한은 소유자 세션 저장 후 발급하며, 제출된 SW 시험 결과는 소유자·세션·문항이 일치하는 경로에서 공개한다.

검증 결과

Swagger UI에서 원본 전후 SQL 풀이 서비스 함수를 같은 저장소 대역으로 다시 실행했다. 합성 객관식 1문항에 active·grading·expired·알 수 없는 상태의 4개 입력을 적용했다.

각 미제출 시험 우회 요청의 결과수정 전수정 후
허용된 입력4/4개0/4개
HTTP 상태201403
정답 데이터 조회1회0회
풀이 저장1회0회
응답의 보호 필드7개0개

보호 필드는 correctAnswers, explanation, scoringCriteria, requiredConcepts, acceptableAlternatives, deductionConditions, errorConditions 7개다.

원본 풀이 서비스 재실행: 4상태 HTTP 201→403, 조회·저장·보호 필드 0원본 보기 (새 창)

원본 풀이 서비스 재실행: 4상태 HTTP 201→403, 조회·저장·보호 필드 0

100%원본 보기 (새 창)

확대한 이미지는 상하좌우로 이동할 수 있습니다. 키보드는 이미지 영역에 초점을 두고 방향키를 사용하세요. Esc로 닫습니다.

추가 입력·조건수정 전수정 후
시험에 속하지 않은 정상 SQL 연습201, 피드백 반환201, 피드백 반환
다른 사용자·엔진·문항·세션의 서명 권한요청과 서명의 연결 검사 없이 진행각각 서명 검증 거부
권한 만료·발급 시각 조건 위반서명 권한 검사 없이 진행검증 거부
제출된 시험의 소유자 결과 조회별도 시험 결과 경로소유자·문항 일치 시 공개 유지

SQL·SW의 객관식·서술형·혼합 시험에도 같은 공개 경계를 적용하는 검사를 포함했다.