같은 private note가 API에서는 필터링됐지만 웹 link 상세 화면에서는 그대로 렌더링됐습니다. 두 사용자와 두 인터페이스를 교차해 visibility 정책의 누락 지점을 찾았습니다.
Taegu Ha · LinkAce · CWE-285 · CVSS 6.5
2026년 3월 27일 공개 · LinkAce 2.5.3에서 수정 · 공식 기록
한눈에 보기공격 흐름취약점 개요조사 대상원인 분석재현 및 검증판단 변화부모 권한API와 웹저장 데이터영향과 수정확인한 범위제외한 범위수정분석 기준과 증거 보존관련 파일2026-08-23 최종 감사느낀 점
한눈에 보기
LinkAce API는 note visibility scope를 적용했지만 web template은
$link->notes를 직접 순회했습니다. link 자체를 볼 수 있는 사용자가 다른 사람의 private note까지 볼 수 있었습니다.구분 | 확인 내용 |
프로젝트·컴포넌트 | LinkAce web link detail · note rendering |
공격 입력 | 공유·내부 link 페이지 조회 |
필요 조건 | link는 볼 수 있지만 그 link의 private note 권한은 없는 사용자 |
취약 지점 | web template의 raw notes relation과 API의 visibleForUser() 사이 정책 불일치 |
검증 결과 | 권한 없는 private note가 web response에 렌더링됨 |
영향 | private note confidentiality 침해 |
수정 원칙 | web·API 모두 동일한 visibility scope와 authorization helper 사용 |
공격 흐름
검증 결론: 같은 데이터가 API에서는 필터링되지만 web view에서는 필터링되지 않아 presentation 경로별 authorization drift를 확인했습니다.
취약점 개요
LinkAce의 link와 note는 각각 공개 범위를 가집니다. 다른 사용자가 볼 수 있는 internal 또는 public link에 소유자만 볼 private note를 붙일 수 있습니다. 이 경우 parent link가 보인다는 사실이 child note의 공개까지 의미하지 않습니다.
API note 경로는
visibleForUser()를 적용해 타인의 private note를 제외했지만, 웹 상세 template은 $link->notes collection을 그대로 순회했습니다. 공격자는 link를 볼 권한만으로 그 아래 private note 본문까지 읽을 수 있었습니다.공격자가 갖는 권한 | 다른 사용자의 internal/public link 열람 |
보호할 객체 | 그 link에 연결된 private note |
취약 인터페이스 | 웹 link detail page |
정상 대조군 | note API의 visibleForUser() 필터 |
조사 대상
부모 객체와 자식 객체의 visibility가 독립적인 모델을 우선 확인했습니다. controller가 parent link에 대해 authorize한 뒤 relation을 eager/lazy load할 때 child scope가 자동으로 붙는지 추적했습니다.
LinkController::show()는 lists와 tags에는 visibleForUser()를 명시했지만 notes에는 같은 scope를 적용하지 않았습니다. template은 전달받은 $link->notes를 별도 검사 없이 출력했습니다.부모 link를 볼 수 있지만 child note는 볼 수 없는 사용자가 인터페이스에 따라 다른 결과를 받는가?
원인 분석
victim 계정으로 internal link와 private note를 만들고 attacker 계정에는 link만 보이도록 했습니다. 여기서 link 자체까지 private이면 controller에서 막혀 note 렌더링 경로에 도달하지 않으므로 적절한 경계 입력이 아닙니다.
attacker가 API로 note를 조회했을 때는 private note가 빠졌지만, 같은 link ID의 웹 상세 화면 HTML에는 note 본문이 포함됐습니다. database row나 policy가 잘못된 것이 아니라 controller별 query scope 적용이 달랐습니다.
웹 template과 API query의 차이
// web: link-notes.blade.php @foreach($link->notes as $note) @include('models.notes.partials.single', ['note' => $note]) @endforeach // API path Note::query()->visibleForUser()->... // web relation에는 visibleForUser()가 없음
입력에서 영향까지의 경로
- 피해자가 공개 가능한 link에 private note를 추가합니다.
- 공격자는 parent link를 볼 수 있습니다.
- 웹 controller가 link를 authorize합니다.
- notes relation이 visibility scope 없이 로드됩니다.
- template이 private note를 HTML에 렌더링합니다.
재현 및 검증
두 사용자, link 공개 범위 두 종류, note private 범위를 조합했습니다. 피해자 화면에서 note가 존재하는지 확인한 뒤 공격자 세션으로 API와 web route를 각각 요청했습니다.
응답 화면만 캡처하지 않고 API JSON과 웹 HTML 원문에서 marker를 검색했습니다. 같은 계정·같은 link·같은 note를 사용해 인터페이스 이외의 변수를 고정했습니다.
$ curl -b attacker.cookie /api/v2/links/42/notes $ curl -b attacker.cookie /links/42
검사 | 입력·조건 | 관찰 | 의미 |
소유자 | victim → web/API | private note 표시 | 데이터가 정상 생성됨 |
API 대조군 | attacker → note API | private note 없음 | visibility policy 존재 |
웹 경로 | attacker → link detail | private marker 표시 | web relation에 필터 누락 |
Private link | attacker → private parent | 접근 거부 | parent policy와 child leak 구분 |
같은 attacker 세션에서의 인터페이스 차이
GET /api/v2/links/42/notes 200 OK [] GET /links/42 200 OK ... <div class="note">VICTIM_PRIVATE_NOTE</div> ...
판단 변화
부모 권한
처음에는 다른 사용자의 link를 볼 수 있다는 점 자체를 문제로 볼 수 있었습니다. 하지만 internal/public link는 공유가 의도된 상태였습니다. 문제를 parent link 노출이 아니라 독립적인 private child note 노출로 좁혔습니다.
API와 웹
API가 안전하다는 결과만 보고 전체 제품이 안전하다고 결론내릴 수 없었습니다. 동일 모델을 렌더링하는 web controller와 template을 따로 확인하면서 정책이 query별로 수동 적용된다는 사실을 찾았습니다.
저장 데이터
DB에서 note의 visibility 값이 잘못 저장됐을 가능성을 배제하기 위해 owner 화면과 API 필터를 확인했습니다. 두 결과가 정상이라 저장 문제가 아니라 web read path 문제로 판단했습니다.
영향과 수정
인증된 사용자는 자신이 볼 수 있는 다른 사용자의 link 상세 페이지에서 그 사용자의 private note를 읽을 수 있었습니다. 링크에 내부 메모나 자격 정보가 기록된 경우 기밀성이 침해됩니다.
확인한 범위
- API에서는 숨겨지는 동일 note의 웹 HTML 노출
- 두 계정으로 소유자·비소유자 경계 재현
- parent와 child visibility의 독립성
제외한 범위
- 비인증 사용자
- private parent link 우회
- note 수정·삭제
수정
2.5.3에서는 web detail path에서도 notes relation에 사용자 visibility scope를 적용했습니다. 같은 모델을 여러 controller에서 읽을 때 scope를 호출자 기억에 맡기기보다 relation 또는 policy 계층에서 일관되게 적용하는 방향이 중요합니다.
분석 기준과 증거 보존
항목 | 최종 감사 결과 |
소스 기준 | LinkAce checkout 605ce0401036bb54408f043ebd3f8ee0366fa036 |
원본 검증 | web template과 API serializer/query scope 비교, 권한 없는 사용자 관점의 렌더 결과 확인 |
GitHub 보존 상태 | Public 저장소 main의 감사 commit c04cacf; evidence 5개와 root manifest.json에 size·SHA-256 고정 |
무결성 | 사례별 SHA256SUMS 검증 통과, root manifest의 모든 file hash와 실제 파일 일치, README 상대 링크 확인 완료 |
보존 경계·제약 | 실제 사용자 note, database dump와 인증 cookie는 제외 |
감사 완료 | 2026-08-23 · Docker/로컬 원본 대조 후 GitHub push 완료 |
관련 파일
2026-08-23 최종 감사
웹 controller와 note template의 조사 스냅샷, 공개 advisory와 CVE 레코드를 포함했습니다.
- LinkController.php — link detail relation loading을 확인한 controller
- link-notes.blade.php — notes collection을 직접 렌더링하던 template
- github-advisory.json — 공개 보고서와 재현 정보
- cve-record.json — CVEProject 공개 레코드
느낀 점
데이터의 공개 범위는 row 하나에 붙은 속성이 아니라 관계를 따라갈 때마다 다시 확인해야 하는 조건이었습니다. 부모를 볼 수 있다는 이유로 자식도 볼 수 있다고 가정하는 순간 privacy 경계가 무너졌습니다.
API와 웹을 같은 입력으로 교차한 방법도 이후 조사에 남았습니다. 한 인터페이스의 안전한 구현은 정답지가 되어 다른 인터페이스에서 빠진 scope를 빠르게 찾게 해주었습니다.