- 분야: 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 상태 | 201 | 403 |
| 정답 데이터 조회 | 1회 | 0회 |
| 풀이 저장 | 1회 | 0회 |
| 응답의 보호 필드 | 7개 | 0개 |
보호 필드는 correctAnswers, explanation, scoringCriteria, requiredConcepts, acceptableAlternatives, deductionConditions, errorConditions 7개다.
| 추가 입력·조건 | 수정 전 | 수정 후 |
|---|---|---|
| 시험에 속하지 않은 정상 SQL 연습 | 201, 피드백 반환 | 201, 피드백 반환 |
| 다른 사용자·엔진·문항·세션의 서명 권한 | 요청과 서명의 연결 검사 없이 진행 | 각각 서명 검증 거부 |
| 권한 만료·발급 시각 조건 위반 | 서명 권한 검사 없이 진행 | 검증 거부 |
| 제출된 시험의 소유자 결과 조회 | 별도 시험 결과 경로 | 소유자·문항 일치 시 공개 유지 |
SQL·SW의 객관식·서술형·혼합 시험에도 같은 공개 경계를 적용하는 검사를 포함했다.