Mass Assignment는 클라이언트가 보낸 필드를 서버가 그대로 모델에 매핑해, 의도하지 않은 컬럼까지 수정되는 결함이다.
AI 코딩 도구는 짧은 코드를 선호해서 DRF에 fields = "__all__", Express에 Object.assign(user, req.body), FastAPI에 model.dict() 통째 저장을 자주 만든다. is_admin·role·balance 같은 컬럼이 PATCH 요청에 그대로 들린다.
OWASP API Security Top 10 2023의 API3:2023 — Broken Object Property Level Authorization(BOPLA)이 정확히 이 결함이며, 2019년판 API6에서 명시된 "Mass Assignment"가 흡수된 항목이다(출처: OWASP API Security Top 10 2023 공식 문서).
한눈에 보는 Mass Assignment
Q. AI가 만든 CRUD에서 왜 자주 나오나요? A. AI 도구는 "전체 필드를 한 번에 받는" 짧은 코드를 선호합니다. DRF의fields = "__all__", Express의 { ...req.body } 패턴이 권장처럼 보여서 검토 없이 운영에 박힙니다.
Q. PATCH 한 줄로 어떻게 관리자가 되나요?
A. PATCH /api/users/me에 {"is_admin": true}를 추가로 보내면, 서버가 화이트리스트 없이 그대로 저장합니다. 프런트엔드에 노출된 필드만 받는 게 아니라 모든 컬럼이 열려 있기 때문입니다.
Q. IDOR(BOLA)과 뭐가 다른가요?
A. BOLA는 "남의 객체에 접근"하는 결함이고, Mass Assignment(BOPLA)는 "본인 객체에서 만질 수 없어야 할 속성"을 만지는 결함입니다. 두 개가 동시에 터지면 임의 계정을 관리자로 만들 수 있습니다.
Q. ORM이 알아서 막아 주지 않나요?
A. 막아 주지 않습니다. Django/Express/FastAPI/Prisma 모두 기본 동작은 "받은 필드 그대로 저장"입니다. 차단은 시리얼라이저·Pydantic·Zod 스키마 레벨에서 명시해야 합니다.
실제로 어떻게 터지나 — PATCH 한 줄 시연
가장 흔한 패턴은 사용자 프로필 수정 API다. 화면에는 닉네임만 있지만, 백엔드가 화이트리스트 없이 받으면 추가 필드가 그대로 들어간다.
# 정상 요청 (프런트 화면 기준)
PATCH /api/users/me
{"nickname": "alice"}
# 공격 요청 — 한 줄 추가
PATCH /api/users/me
{"nickname": "alice", "is_admin": true, "credit": 999999}
핵심 원리: 서버가 request.body를 그대로 모델에 매핑하면, 화면에 없는 컬럼도 다 열린다. 프런트 폼에 없으니 안전하다는 가정이 가장 흔한 오해다.
AI가 자주 만드는 위험 패턴 5가지
| 스택 | AI가 자주 짜는 코드 | 위험 컬럼 예시 |
|---|---|---|
| Django REST Framework | class UserSerializer(ModelSerializer): class Meta: fields = "__all__" | is_staff, is_superuser, user_permissions |
| Express + Mongoose | User.findByIdAndUpdate(id, req.body) | role, balance, verified |
| FastAPI + SQLAlchemy | for k,v in data.dict().items(): setattr(user,k,v) | is_admin, plan_tier, stripe_customer_id |
| Next.js Server Action | await prisma.user.update({data: formData}) | role, emailVerified, plan |
| Ruby on Rails | @user.update(params[:user]) (strong_params 미사용) | admin, points, organization_id |
30초 자가 점검 — 새고 있는지 확인
운영 중인 API에 다음 3가지를 시도해 본다. 하나라도 응답·DB에 반영되면 새고 있다.
# 1. 본인 계정에 관리자 플래그 시도
curl -X PATCH https://your.app/api/users/me \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"is_admin":true,"role":"admin","is_staff":true}'
# 2. 잔액·플랜 등 비즈니스 컬럼 시도
curl -X PATCH https://your.app/api/users/me \
-H "Authorization: Bearer $TOKEN" \
-d '{"credit":99999999,"plan":"enterprise","verified":true}'
# 3. 소유권 이전 시도 (IDOR + Mass Assignment 결합)
curl -X PATCH https://your.app/api/posts/123 \
-H "Authorization: Bearer $TOKEN" \
-d '{"author_id":"<<공격자 ID>>"}'
위 3가지는 인증이 필요해 외부 자동 점검으로 잡기 어렵다. 그러나 진입 단계인 .env·관리자 페이지·민감 파일·헤더 17종은 CodeScan 30초 무료 스캔으로 한 번에 잡힌다. 사내 SAST가 없는 팀이라면 우선 외부 노출부터 끊는 게 비용 대비 효과가 가장 크다.
스택별 차단 코드 — 복사해서 바로 적용
Django REST Framework
class UserUpdateSerializer(serializers.ModelSerializer):
class Meta:
model = User
# fields="__all__" 금지. 클라이언트가 만질 수 있는 것만 명시
fields = ["nickname", "avatar_url", "bio"]
# read_only_fields 는 보조 방어선
read_only_fields = ["is_staff", "is_superuser", "date_joined"]
Express + Mongoose
// 화이트리스트 헬퍼
const pick = (obj, keys) => keys.reduce((a,k)=> (k in obj ? {...a,[k]:obj[k]} : a), {});
router.patch("/users/me", auth, async (req, res) => {
const allowed = pick(req.body, ["nickname", "avatar_url", "bio"]);
const user = await User.findByIdAndUpdate(req.user.id, allowed, {new:true});
res.json(user);
});
FastAPI + Pydantic
class UserUpdate(BaseModel):
nickname: str | None = None
avatar_url: str | None = None
bio: str | None = None
model_config = ConfigDict(extra="forbid") # 정의 안 된 필드는 400
@app.patch("/users/me")
def update_me(payload: UserUpdate, user=Depends(current_user)):
for k, v in payload.model_dump(exclude_unset=True).items():
setattr(user, k, v)
db.commit()
return user
Next.js Server Action + Prisma
const schema = z.object({
nickname: z.string().min(1).max(40),
bio: z.string().max(500).optional(),
});
export async function updateProfile(formData: FormData) {
const parsed = schema.parse(Object.fromEntries(formData));
return prisma.user.update({
where: { id: session.user.id },
data: parsed, // role·emailVerified 는 스키마에 없으니 들어올 수 없음
});
}
실제 보안 사고 사례
Mass Assignment 는 GitHub·HackerOne·정부 보고서에서 반복되는 결함이다.
- GitHub 사고 (2012) — Rails strong_params 도입 이전, 한 연구자가 PATCH 로 자신의 SSH 공개키를 Rails 코어 organization 에 등록해 임의 코드 푸시가 가능했다(출처: github.blog "Public Key Security Vulnerability and Mitigation").
- HackerOne 디스클로저 — 사용자 프로필 PATCH 에
is_admin·role필드를 추가해 관리자 권한을 얻은 보고가 매년 수십 건 공개된다(출처: HackerOne Hacktivity, "mass assignment" 태그). - OWASP API Security Top 10 2023 — API3 BOPLA 를 "공격자가 객체 속성을 변경해 권한 상승하거나 민감 데이터에 접근하는 결함"으로 정의하며, 가장 흔히 발견되는 결함 중 하나로 분류한다(출처: owasp.org/API-Security/editions/2023).
비슷한 결함과 비교
| 결함 | OWASP | 본질 | 대표 증상 |
|---|---|---|---|
| Mass Assignment (BOPLA) | API3:2023 | 속성 단위 권한 누락 | PATCH 에 is_admin:true 추가하면 통과 |
| BOLA (IDOR) | API1:2023 | 객체 단위 권한 누락 | GET /users/999 로 남의 정보 열람 |
| Broken Function Level Auth | API5:2023 | 엔드포인트 단위 권한 누락 | 일반 사용자가 /admin/* 접근 |
| Excessive Data Exposure | API3:2019 | 응답 과다 노출 | GET 응답에 비밀번호 해시 포함 |
발견했다면 — 사고 대응 4단계
- 로그 역추적 — 최근 90일 PATCH/PUT 로그에서
is_admin·role·balance등 위험 키워드를 검색해 영향 받은 계정 식별. - 권한 원복 — 비정상 권한이 부여된 계정의 role·플래그를 일괄 원복.
- 화이트리스트 강제 — 위 스택별 차단 코드를 즉시 배포.
fields="__all__"·req.bodyspread 를 전체 코드베이스에서 grep 해 제거. - 회귀 테스트 추가 — "본인 PATCH 에
is_admin:true보내면 권한이 안 바뀐다"를 통합 테스트로 박아 두기. AI 가 다시 짜도 막힌다.
오늘 시작할 5가지 액션
grep -rn 'fields\s*=\s*"__all__"\|req\.body)\s*$\|Object\.assign(.*req\.body' src/로 위험 패턴 검색- 위 스택별 코드 중 본인 스택 1개를 복사해 화이트리스트 시리얼라이저 작성
- 본인 계정에
{"is_admin":true,"role":"admin"}PATCH 직접 시도 → 응답·DB 양쪽 확인 - 관련 글: AI 가 만든 JWT 인증의 흔한 실수, Supabase RLS 우회 사례, API 키 유출 30초 점검 — 권한 결함은 보통 함께 터진다
- 외부 노출은 CodeScan 30초 무료 스캔으로, 인증 후 결함은 사내 회귀 테스트로 이중 방어
CodeScan 이 잡아 주는 것
CodeScan 은 해킹대회 수상·레드팀 운영 경력의 보안 전문 기업 SENTRIX 가 운영하는 보안 점검 SaaS 다.
URL 하나만 넣으면 외부 노출 자산·환경변수·민감 파일·헤더 17종을 자동 점검한다.
- 무료 스캔:
.env·관리자 페이지·민감 파일·헤더 17종 등 외부 노출 1차 점검 - 정기 스캔: 코드 변경마다 자동 점검 + 이상 시 알람
- 사내 SAST 연동(엔터프라이즈):
fields="__all__"·req.bodyspread 패턴까지 자동 시그니처화
다음에 해야 할 한 가지
Mass Assignment 는 "화면에 없는 컬럼이 열려 있다"는 단순한 사실에서 시작된다. AI 가 짜 준 CRUD 를 한 번도 검토하지 않았다면, 본인 계정 PATCH 에 is_admin:true 를 추가해 보는 것이 가장 빠른 확인이다.
👉 실무자라면 지금 CodeScan 30초 무료 스캔으로 외부 노출부터 끊고, 본 글의 스택별 코드 1개를 오늘 배포하라.
👉 임원/CISO 라면 SENTRIX 30분 1:1 API 보안 진단 상담으로 BOPLA 포함 OWASP API Top 10 전체를 1회 점검받는 것을 권장한다.
{ "@context": "https://schema.org", "@type": "FAQPage", "mainEntity": [ {"@type":"Question","name":"Mass Assignment 취약점이 무엇인가요?","acceptedAnswer":{"@type":"Answer","text":"클라이언트가 보낸 필드를 서버가 화이트리스트 없이 그대로 모델에 매핑해, is_admin·role 같은 의도하지 않은 컬럼까지 수정되는 결함입니다. OWASP API Security Top 10 2023의 API3 BOPLA로 분류됩니다."}}, {"@type":"Question","name":"AI 코딩으로 만든 API에서 왜 자주 발생하나요?","acceptedAnswer":{"@type":"Answer","text":"AI 도구는 짧은 코드를 선호해 DRF의 fields=__all__, Express의 req.body 통째 spread를 자주 만듭니다. 검토 없이 배포되면 화면에 없는 컬럼까지 PATCH로 열립니다."}}, {"@type":"Question","name":"PATCH 한 줄로 관리자 권한을 얻을 수 있나요?","acceptedAnswer":{"@type":"Answer","text":"화이트리스트 없는 API라면 PATCH /api/users/me에 is_admin:true를 추가해 보내는 것만으로 관리자 권한이 부여될 수 있습니다."}}, {"@type":"Question","name":"IDOR(BOLA)과 어떻게 다른가요?","acceptedAnswer":{"@type":"Answer","text":"BOLA는 남의 객체 접근, Mass Assignment(BOPLA)는 본인 객체의 만질 수 없어야 할 속성 변경입니다. 두 개가 함께 터지면 임의 계정을 관리자로 만들 수 있습니다."}}, {"@type":"Question","name":"ORM이 자동으로 막아 주지 않나요?","acceptedAnswer":{"@type":"Answer","text":"막아 주지 않습니다. Django/Express/FastAPI/Prisma 모두 기본은 받은 필드 그대로 저장이며, 화이트리스트는 시리얼라이저·Pydantic·Zod 스키마에서 명시해야 합니다."}} ] }