mirror of
https://github.com/HackTricks-wiki/hacktricks-cloud.git
synced 2026-03-12 21:22:57 -07:00
Translated ['', 'src/pentesting-ci-cd/supabase-security.md'] to ko
This commit is contained in:
@@ -4,46 +4,46 @@
|
||||
|
||||
## 기본 정보
|
||||
|
||||
그들의 [**랜딩 페이지**](https://supabase.com/)에 따르면: Supabase는 오픈 소스 Firebase 대안입니다. Postgres 데이터베이스, 인증, 즉시 API, 엣지 함수, 실시간 구독, 스토리지 및 벡터 임베딩으로 프로젝트를 시작하세요.
|
||||
As per their [**landing page**](https://supabase.com/): Supabase is an open source Firebase alternative. Start your project with a Postgres database, Authentication, instant APIs, Edge Functions, Realtime subscriptions, Storage, and Vector embeddings.
|
||||
|
||||
### 서브도메인
|
||||
|
||||
기본적으로 프로젝트가 생성되면 사용자는 **`jnanozjdybtpqgcwhdiz.supabase.co`**와 같은 supabase.co 서브도메인을 받게 됩니다.
|
||||
기본적으로 프로젝트가 생성되면 사용자에게 다음과 같은 supabase.co 서브도메인이 부여됩니다: **`jnanozjdybtpqgcwhdiz.supabase.co`**
|
||||
|
||||
## **데이터베이스 구성**
|
||||
|
||||
> [!TIP]
|
||||
> **이 데이터는 `https://supabase.com/dashboard/project/<project-id>/settings/database`와 같은 링크에서 접근할 수 있습니다.**
|
||||
> **이 데이터는 `https://supabase.com/dashboard/project/<project-id>/settings/database` 같은 링크에서 접근할 수 있습니다**
|
||||
|
||||
이 **데이터베이스**는 일부 AWS 리전에서 배포되며, 연결하기 위해서는 다음과 같이 연결할 수 있습니다: `postgres://postgres.jnanozjdybtpqgcwhdiz:[YOUR-PASSWORD]@aws-0-us-west-1.pooler.supabase.com:5432/postgres` (이것은 us-west-1에서 생성되었습니다).\
|
||||
비밀번호는 사용자가 이전에 입력한 **비밀번호**입니다.
|
||||
이 **데이터베이스**는 특정 AWS 리전에 배포되며, 연결하려면 다음과 같은 주소로 연결하면 됩니다: `postgres://postgres.jnanozjdybtpqgcwhdiz:[YOUR-PASSWORD]@aws-0-us-west-1.pooler.supabase.com:5432/postgres` (이 예시는 us-west-1에 생성되었습니다).
|
||||
비밀번호는 **사용자가 이전에 설정한 비밀번호**입니다.
|
||||
|
||||
따라서 서브도메인이 알려진 것이고 사용자 이름으로 사용되며 AWS 리전이 제한적이기 때문에 **비밀번호를 무작위 대입 공격**할 가능성이 있을 수 있습니다.
|
||||
따라서 서브도메인이 알려져 있고 사용자 이름으로 사용되며 AWS 리전이 제한적이기 때문에 **brute force the password**를 시도해 볼 수 있습니다.
|
||||
|
||||
이 섹션에는 다음과 같은 옵션도 포함되어 있습니다:
|
||||
이 섹션에는 다음 옵션들도 포함됩니다:
|
||||
|
||||
- 데이터베이스 비밀번호 재설정
|
||||
- 연결 풀링 구성
|
||||
- SSL 구성: 평문 연결 거부 (기본적으로 활성화되어 있음)
|
||||
- 연결 풀(connection pooling) 구성
|
||||
- SSL 구성: 평문 연결 거부(기본값으로 평문 연결이 허용되어 있음)
|
||||
- 디스크 크기 구성
|
||||
- 네트워크 제한 및 금지 적용
|
||||
- 네트워크 제한 및 차단 적용
|
||||
|
||||
## API 구성
|
||||
|
||||
> [!TIP]
|
||||
> **이 데이터는 `https://supabase.com/dashboard/project/<project-id>/settings/api`와 같은 링크에서 접근할 수 있습니다.**
|
||||
> **이 데이터는 `https://supabase.com/dashboard/project/<project-id>/settings/api` 같은 링크에서 접근할 수 있습니다**
|
||||
|
||||
프로젝트에서 supabase API에 접근하는 URL은 다음과 같을 것입니다: `https://jnanozjdybtpqgcwhdiz.supabase.co`.
|
||||
프로젝트의 supabase API에 접근하는 URL은 다음과 같습니다: `https://jnanozjdybtpqgcwhdiz.supabase.co`.
|
||||
|
||||
### anon API 키
|
||||
|
||||
또한 **anon API 키**(`role: "anon"`)를 생성합니다, 예: `eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6ImpuYW5vemRyb2J0cHFnY3doZGl6Iiwicm9sZSI6ImFub24iLCJpYXQiOjE3MTQ5OTI3MTksImV4cCI6MjAzMDU2ODcxOX0.sRN0iMGM5J741pXav7UxeChyqBE9_Z-T0tLA9Zehvqk` 이 애플리케이션이 API 키에 접근하기 위해 사용해야 합니다.
|
||||
또한 **anon API key** (`role: "anon"`), 예: `eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6ImpuYW5vemRyb2J0cHFnY3doZGl6Iiwicm9sZSI6ImFub24iLCJpYXQiOjE3MTQ5OTI3MTksImV4cCI6MjAzMDU2ODcxOX0.sRN0iMGM5J741pXav7UxeChyqBE9_Z-T0tLA9Zehvqk` 이 생성되며, 애플리케이션이 API에 접근하기 위해 사용해야 합니다.
|
||||
|
||||
이 API에 연락하기 위한 API REST를 [**문서**](https://supabase.com/docs/reference/self-hosting-auth/returns-the-configuration-settings-for-the-gotrue-server)에서 찾을 수 있지만, 가장 흥미로운 엔드포인트는 다음과 같습니다:
|
||||
이 API와 통신할 REST API는 [**docs**](https://supabase.com/docs/reference/self-hosting-auth/returns-the-configuration-settings-for-the-gotrue-server)에서 확인할 수 있지만, 가장 흥미로운 엔드포인트는 다음과 같습니다:
|
||||
|
||||
<details>
|
||||
|
||||
<summary>가입 (/auth/v1/signup)</summary>
|
||||
<summary>회원가입 (/auth/v1/signup)</summary>
|
||||
```
|
||||
POST /auth/v1/signup HTTP/2
|
||||
Host: id.io.net
|
||||
@@ -99,61 +99,171 @@ Priority: u=1, i
|
||||
```
|
||||
</details>
|
||||
|
||||
그래서, 클라이언트가 부여받은 서브도메인으로 supabase를 사용하는 것을 발견할 때마다 (회사의 서브도메인이 그들의 supabase 서브도메인에 CNAME을 가질 가능성이 있음), **supabase API를 사용하여 플랫폼에 새 계정을 생성**해 볼 수 있습니다.
|
||||
따라서 클라이언트가 부여받은 서브도메인으로 supabase를 사용하고 있는 것을 발견하면(회사 도메인의 서브도메인이 supabase 서브도메인에 CNAME을 설정했을 가능성이 있음), **supabase API를 사용해 플랫폼에 새 계정을 생성해 볼 수 있습니다**.
|
||||
|
||||
### 비밀 / 서비스 역할 API 키
|
||||
### secret / service_role api keys
|
||||
|
||||
**`role: "service_role"`**로 비밀 API 키도 생성됩니다. 이 API 키는 **Row Level Security**를 우회할 수 있기 때문에 비밀이어야 합니다.
|
||||
A secret API key will also be generated with **`role: "service_role"`**. This API key should be secret because it will be able to bypass **Row Level Security**.
|
||||
|
||||
API 키는 다음과 같습니다: `eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6ImpuYW5vemRyb2J0cHFnY3doZGl6Iiwicm9sZSI6InNlcnZpY2Vfcm9sZSIsImlhdCI6MTcxNDk5MjcxOSwiZXhwIjoyMDMwNTY4NzE5fQ.0a8fHGp3N_GiPq0y0dwfs06ywd-zhTwsm486Tha7354`
|
||||
The API key looks like this: `eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6ImpuYW5vemRyb2J0cHFnY3doZGl6Iiwicm9sZSI6InNlcnZpY2Vfcm9sZSIsImlhdCI6MTcxNDk5MjcxOSwiZXhwIjoyMDMwNTY4NzE5fQ.0a8fHGp3N_GiPq0y0dwfs06ywd-zhTwsm486Tha7354`
|
||||
|
||||
### JWT 비밀
|
||||
### JWT Secret
|
||||
|
||||
**JWT 비밀**도 생성되어 애플리케이션이 **사용자 정의 JWT 토큰을 생성하고 서명**할 수 있습니다.
|
||||
A **JWT Secret** will also be generate so the application can **create and sign custom JWT tokens**.
|
||||
|
||||
## 인증
|
||||
|
||||
### 가입
|
||||
### 회원가입
|
||||
|
||||
> [!TIP]
|
||||
> 기본적으로 supabase는 **새 사용자가 프로젝트에 계정을 생성**할 수 있도록 이전에 언급한 API 엔드포인트를 사용합니다.
|
||||
> 기본적으로 supabase는 앞서 언급한 API 엔드포인트를 사용해 **새 사용자가 프로젝트에 계정을 생성하는 것**을 허용합니다.
|
||||
|
||||
그러나 이러한 새 계정은 기본적으로 **로그인하기 위해 이메일 주소를 확인해야** 합니다. 이메일 주소 확인 없이 로그인할 수 있도록 **"익명 로그인 허용"**을 활성화할 수 있습니다. 이는 **예상치 못한 데이터**에 대한 접근을 허용할 수 있습니다 (그들은 `public` 및 `authenticated` 역할을 받습니다).\
|
||||
이는 supabase가 활성 사용자당 요금을 부과하기 때문에 매우 나쁜 아이디어입니다. 사람들이 사용자를 생성하고 로그인할 수 있으며 supabase는 이에 대해 요금을 부과할 것입니다:
|
||||
그러나 이러한 새 계정은 기본적으로 **로그인하려면 이메일 주소를 검증해야 합니다**. 이메일 검증 없이 로그인할 수 있도록 **"Allow anonymous sign-ins"**를 활성화할 수 있습니다. 이 경우 사용자들은 이메일 확인 없이 로그인할 수 있으며, **예상치 못한 데이터**에 접근할 수 있게 될 수 있습니다(이들은 `public` 및 `authenticated` 역할을 부여받습니다).\
|
||||
이것은 매우 나쁜 생각입니다. supabase는 활성 사용자 수에 따라 요금을 청구하므로 사람들이 계정을 생성하고 로그인하면 supabase에 비용이 발생합니다:
|
||||
|
||||
<figure><img src="../images/image (1) (1) (1) (1) (1) (1).png" alt=""><figcaption></figcaption></figure>
|
||||
|
||||
### 비밀번호 및 세션
|
||||
#### Auth: 서버 측 회원가입 제어
|
||||
|
||||
최소 비밀번호 길이(기본값), 요구 사항(기본값 없음)을 지정하고 유출된 비밀번호 사용을 금지할 수 있습니다.\
|
||||
기본 요구 사항이 약하기 때문에 **요구 사항을 개선하는 것이 좋습니다**.
|
||||
프론트엔드에서 가입 버튼을 숨기는 것만으로는 충분하지 않습니다. **Auth 서버가 여전히 가입을 허용하면**, 공격자는 퍼블릭 `anon` 키로 API를 직접 호출해 임의의 사용자를 생성할 수 있습니다.
|
||||
|
||||
- 사용자 세션: 사용자 세션 작동 방식을 구성할 수 있습니다 (타임아웃, 사용자당 1세션...)
|
||||
- 봇 및 남용 보호: Captcha를 활성화할 수 있습니다.
|
||||
빠른 테스트 (비인증 클라이언트에서):
|
||||
```bash
|
||||
curl -X POST \
|
||||
-H "apikey: <SUPABASE_ANON_KEY>" \
|
||||
-H "Authorization: Bearer <SUPABASE_ANON_KEY>" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"email":"attacker@example.com","password":"Sup3rStr0ng!"}' \
|
||||
https://<PROJECT_REF>.supabase.co/auth/v1/signup
|
||||
```
|
||||
Expected hardening:
|
||||
- Disable email/password signups in the Dashboard: Authentication → Providers → Email → Disable sign ups (invite-only), or set the equivalent GoTrue setting.
|
||||
- API가 이전 호출에 대해 이제 4xx를 반환하고 새로운 사용자가 생성되지 않는지 확인하세요.
|
||||
- invites 또는 SSO에 의존하는 경우, 명시적으로 필요하지 않은 모든 다른 providers는 비활성화되어 있는지 확인하세요.
|
||||
|
||||
### SMTP 설정
|
||||
## RLS 및 Views: PostgREST를 통한 쓰기 우회
|
||||
|
||||
이메일을 보내기 위해 SMTP를 설정할 수 있습니다.
|
||||
Postgres VIEW를 사용해 민감한 컬럼을 “숨기고” 이를 PostgREST로 노출하면 권한 평가 방식이 달라질 수 있습니다. PostgreSQL에서는:
|
||||
- 일반적인 views는 기본적으로 view owner의 권한으로 실행됩니다 (definer semantics). PG ≥15에서는 `security_invoker`를 선택할 수 있습니다.
|
||||
- Row Level Security (RLS)는 base tables에 적용됩니다. Table owners는 테이블에 `FORCE ROW LEVEL SECURITY`가 설정되지 않은 한 RLS를 우회합니다.
|
||||
- Updatable views는 INSERT/UPDATE/DELETE를 받아 base table에 적용될 수 있습니다. `WITH CHECK OPTION`이 없으면 view 조건에 맞지 않는 쓰기 요청도 성공할 수 있습니다.
|
||||
|
||||
### 고급 설정
|
||||
현장에서 관찰된 위험 패턴:
|
||||
- 컬럼을 줄인 view가 Supabase REST를 통해 노출되고 `anon`/`authenticated`에 권한이 부여된다.
|
||||
- PostgREST는 updatable view에 대한 DML을 허용하고, 해당 연산은 view owner의 권한으로 평가되어 base table에 대한 의도된 RLS 정책을 사실상 우회한다.
|
||||
- 결과: 권한이 낮은 클라이언트가 수정해서는 안 되는 행들(예: profile bios/avatars)을 대량으로 편집할 수 있다.
|
||||
|
||||
- 액세스 토큰의 만료 시간 설정 (기본값 3600)
|
||||
- 잠재적으로 손상된 새로 고침 토큰을 감지하고 취소하는 설정 및 타임아웃
|
||||
- MFA: 사용자당 한 번에 등록할 수 있는 MFA 요소 수를 지정 (기본값 10)
|
||||
- 최대 직접 데이터베이스 연결: 인증에 사용되는 최대 연결 수 (기본값 10)
|
||||
- 최대 요청 지속 시간: 인증 요청이 지속될 수 있는 최대 시간 (기본값 10초)
|
||||
Illustrative write via view (attempted from a public client):
|
||||
```bash
|
||||
curl -X PATCH \
|
||||
-H "apikey: <SUPABASE_ANON_KEY>" \
|
||||
-H "Authorization: Bearer <SUPABASE_ANON_KEY>" \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "Prefer: return=representation" \
|
||||
-d '{"bio":"pwned","avatar_url":"https://i.example/pwn.png"}' \
|
||||
"https://<PROJECT_REF>.supabase.co/rest/v1/users_view?id=eq.<victim_user_id>"
|
||||
```
|
||||
Hardening checklist for views and RLS:
|
||||
- Prefer exposing base tables with explicit, least-privilege grants and precise RLS policies.
|
||||
- If you must expose a view:
|
||||
- Make it non-updatable (e.g., include expressions/joins) or deny `INSERT/UPDATE/DELETE` on the view to all untrusted roles.
|
||||
- Enforce `ALTER VIEW <v> SET (security_invoker = on)` so the invoker’s privileges are used instead of the owner’s.
|
||||
- On base tables, use `ALTER TABLE <t> FORCE ROW LEVEL SECURITY;` so even owners are subject to RLS.
|
||||
- If allowing writes via an updatable view, add `WITH [LOCAL|CASCADED] CHECK OPTION` and complementary RLS on base tables to ensure only allowed rows can be written/changed.
|
||||
- In Supabase, avoid granting `anon`/`authenticated` any write privileges on views unless you have verified end-to-end behavior with tests.
|
||||
|
||||
## 저장소
|
||||
Detection tip:
|
||||
- From `anon` and an `authenticated` test user, attempt all CRUD operations against every exposed table/view. Any successful write where you expected denial indicates a misconfiguration.
|
||||
|
||||
### OpenAPI-driven CRUD probing from anon/auth roles
|
||||
|
||||
PostgREST exposes an OpenAPI document that you can use to enumerate all REST resources, then automatically probe allowed operations from low-privileged roles.
|
||||
|
||||
Fetch the OpenAPI (works with the public anon key):
|
||||
```bash
|
||||
curl -s https://<PROJECT_REF>.supabase.co/rest/v1/ \
|
||||
-H "apikey: <SUPABASE_ANON_KEY>" \
|
||||
-H "Authorization: Bearer <SUPABASE_ANON_KEY>" \
|
||||
-H "Accept: application/openapi+json" | jq '.paths | keys[]'
|
||||
```
|
||||
프로브 패턴 (예시):
|
||||
- 단일 행 읽기 (RLS에 따라 401/403/200 예상):
|
||||
```bash
|
||||
curl -s "https://<PROJECT_REF>.supabase.co/rest/v1/<table>?select=*&limit=1" \
|
||||
-H "apikey: <SUPABASE_ANON_KEY>" \
|
||||
-H "Authorization: Bearer <SUPABASE_ANON_KEY>"
|
||||
```
|
||||
- UPDATE가 차단되었는지 테스트 (테스트 중 데이터 변경을 피하려면 존재하지 않는 filter를 사용하세요):
|
||||
```bash
|
||||
curl -i -X PATCH \
|
||||
-H "apikey: <SUPABASE_ANON_KEY>" \
|
||||
-H "Authorization: Bearer <SUPABASE_ANON_KEY>" \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "Prefer: return=minimal" \
|
||||
-d '{"__probe":true}' \
|
||||
"https://<PROJECT_REF>.supabase.co/rest/v1/<table_or_view>?id=eq.00000000-0000-0000-0000-000000000000"
|
||||
```
|
||||
- INSERT 테스트가 차단됨:
|
||||
```bash
|
||||
curl -i -X POST \
|
||||
-H "apikey: <SUPABASE_ANON_KEY>" \
|
||||
-H "Authorization: Bearer <SUPABASE_ANON_KEY>" \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "Prefer: return=minimal" \
|
||||
-d '{"__probe":true}' \
|
||||
"https://<PROJECT_REF>.supabase.co/rest/v1/<table_or_view>"
|
||||
```
|
||||
- DELETE 테스트가 차단됨:
|
||||
```bash
|
||||
curl -i -X DELETE \
|
||||
-H "apikey: <SUPABASE_ANON_KEY>" \
|
||||
-H "Authorization: Bearer <SUPABASE_ANON_KEY>" \
|
||||
"https://<PROJECT_REF>.supabase.co/rest/v1/<table_or_view>?id=eq.00000000-0000-0000-0000-000000000000"
|
||||
```
|
||||
Recommendations:
|
||||
- Automate the previous probes for both `anon` and a minimally `authenticated` user and integrate them in CI to catch regressions.
|
||||
- Treat every exposed table/view/function as a first-class surface. Don’t assume a view “inherits” the same RLS posture as its base tables.
|
||||
|
||||
### Passwords & sessions
|
||||
|
||||
최소 비밀번호 길이(기본값), 요구사항(기본적으로 없음)을 지정하고 leaked passwords 사용을 금지할 수 있습니다.
|
||||
기본 요구사항이 약하므로 **요구사항을 강화하는 것이 권장됩니다**.
|
||||
|
||||
- User Sessions: 사용자 세션 동작(타임아웃, 1 사용자당 1 세션 등)을 구성할 수 있습니다.
|
||||
- Bot and Abuse Protection: Captcha를 활성화할 수 있습니다.
|
||||
|
||||
### SMTP Settings
|
||||
|
||||
이메일 전송을 위해 SMTP를 설정할 수 있습니다.
|
||||
|
||||
### Advanced Settings
|
||||
|
||||
- Set expire time to access tokens (3600 by default)
|
||||
- 잠재적으로 compromised된 refresh tokens를 탐지하고 폐기하며 타임아웃을 설정합니다
|
||||
- MFA: 사용자당 동시에 등록할 수 있는 MFA 요소 수를 지정합니다(기본값 10)
|
||||
- Max Direct Database Connections: 인증에 사용되는 최대 연결 수(기본값 10)
|
||||
- Max Request Duration: Auth 요청이 허용되는 최대 지속 시간(기본값 10s)
|
||||
|
||||
## Storage
|
||||
|
||||
> [!TIP]
|
||||
> Supabase는 **파일을 저장**하고 URL을 통해 접근할 수 있도록 합니다 (S3 버킷을 사용합니다).
|
||||
> Supabase는 파일을 저장하고 URL을 통해 접근 가능하게 만들 수 있습니다 (S3 buckets를 사용합니다).
|
||||
|
||||
- 업로드 파일 크기 제한 설정 (기본값 50MB)
|
||||
- S3 연결은 다음과 같은 URL로 제공됩니다: `https://jnanozjdybtpqgcwhdiz.supabase.co/storage/v1/s3`
|
||||
- `access key ID` (예: `a37d96544d82ba90057e0e06131d0a7b`)와 `secret access key` (예: `58420818223133077c2cec6712a4f909aec93b4daeedae205aa8e30d5a860628`)로 구성된 **S3 액세스 키를 요청**할 수 있습니다.
|
||||
- 업로드 파일 크기 제한을 설정합니다(기본값 50MB)
|
||||
- The S3 connection is given with a URL like: `https://jnanozjdybtpqgcwhdiz.supabase.co/storage/v1/s3`
|
||||
- It's possible to **request S3 access key** that are formed by an `access key ID` (e.g. `a37d96544d82ba90057e0e06131d0a7b`) and a `secret access key` (e.g. `58420818223133077c2cec6712a4f909aec93b4daeedae205aa8e30d5a860628`)
|
||||
|
||||
## 엣지 함수
|
||||
## Edge Functions
|
||||
|
||||
supabase에 **비밀을 저장**할 수 있으며, 이는 **엣지 함수에 의해 접근 가능**합니다 (웹에서 생성 및 삭제할 수 있지만, 그 값에 직접 접근할 수는 없습니다).
|
||||
supabase에도 **secrets를 저장**할 수 있으며 이는 **edge functions에서 접근 가능**합니다(웹에서 생성 및 삭제할 수 있지만, 값 자체에는 직접 접근할 수 없습니다).
|
||||
|
||||
## References
|
||||
|
||||
- [Building Hacker Communities: Bug Bounty Village, getDisclosed’s Supabase Misconfig, and the LHE Squad (Ep. 133) – YouTube](https://youtu.be/NI-eXMlXma4)
|
||||
- [Critical Thinking Podcast – Episode 133 page](https://www.criticalthinkingpodcast.io/episode-133-building-hacker-communities-bug-bounty-village-getdisclosed-and-the-lhe-squad/)
|
||||
- [Supabase: Row Level Security (RLS)](https://supabase.com/docs/guides/auth/row-level-security)
|
||||
- [PostgreSQL: Row Security Policies](https://www.postgresql.org/docs/current/ddl-rowsecurity.html)
|
||||
- [PostgreSQL: CREATE VIEW (security_invoker, check option)](https://www.postgresql.org/docs/current/sql-createview.html)
|
||||
- [PostgREST: OpenAPI documentation](https://postgrest.org/en/stable/references/api.html#openapi-documentation)
|
||||
|
||||
{{#include ../banners/hacktricks-training.md}}
|
||||
|
||||
Reference in New Issue
Block a user