Security: Enforce mandatory API Keys for V3, fix search logic, and update frontend guide
This commit is contained in:
@@ -166,11 +166,23 @@ def sql_search_qry_part(
|
|||||||
data[p_name] = query_node.query_string
|
data[p_name] = query_node.query_string
|
||||||
elif searchable_fields:
|
elif searchable_fields:
|
||||||
like_clauses = []
|
like_clauses = []
|
||||||
|
# Fields to exclude from a generic text 'q' search (numeric, technical, or date fields)
|
||||||
|
exclude_patterns = [
|
||||||
|
'enable', 'hide', 'priority', 'sort', 'group',
|
||||||
|
'created_on', 'updated_on'
|
||||||
|
]
|
||||||
for field in searchable_fields:
|
for field in searchable_fields:
|
||||||
if not any(x in field for x in ['_id', 'enable', 'hide', 'priority', 'sort', 'group', 'created_on', 'updated_on']):
|
# Exclude exact internal integer IDs (ending in _id)
|
||||||
f_p_name = get_param_name()
|
if field.endswith('_id') or field == 'id':
|
||||||
like_clauses.append(f"`{field}` LIKE :{f_p_name}")
|
continue
|
||||||
data[f_p_name] = f"%{query_node.query_string}%"
|
|
||||||
|
# Exclude other technical/meta fields
|
||||||
|
if any(x == field for x in exclude_patterns):
|
||||||
|
continue
|
||||||
|
|
||||||
|
f_p_name = get_param_name()
|
||||||
|
like_clauses.append(f"`{field}` LIKE :{f_p_name}")
|
||||||
|
data[f_p_name] = f"%{query_node.query_string}%"
|
||||||
if like_clauses: clauses.append(f"({' OR '.join(like_clauses)})")
|
if like_clauses: clauses.append(f"({' OR '.join(like_clauses)})")
|
||||||
for filter_attr in ['and_filters', 'or_filters']:
|
for filter_attr in ['and_filters', 'or_filters']:
|
||||||
if hasattr(query_node, filter_attr) and getattr(query_node, filter_attr):
|
if hasattr(query_node, filter_attr) and getattr(query_node, filter_attr):
|
||||||
|
|||||||
@@ -12,35 +12,63 @@ log = logging.getLogger(__name__)
|
|||||||
def get_account_context_optional(
|
def get_account_context_optional(
|
||||||
x_account_id: Optional[str] = Header(None, min_length=11, max_length=22),
|
x_account_id: Optional[str] = Header(None, min_length=11, max_length=22),
|
||||||
x_no_account_id: Optional[str] = Header(None, min_length=3, max_length=100),
|
x_no_account_id: Optional[str] = Header(None, min_length=3, max_length=100),
|
||||||
x_no_account_id_token: Optional[str] = Query(None, min_length=11, max_length=22),
|
x_no_account_id_token: Optional[str] = Query(None, alias='jwt', min_length=11, max_length=22),
|
||||||
|
x_aether_api_key: Optional[str] = Header(None, min_length=11, max_length=22),
|
||||||
) -> AccountContext:
|
) -> AccountContext:
|
||||||
"""
|
"""
|
||||||
Resolves the account context but does not raise 403 on failure.
|
Resolves the account context and enforces API Key validation.
|
||||||
Uses DEFERRED imports to prevent circular dependency at startup.
|
Uses DEFERRED imports to prevent circular dependency at startup.
|
||||||
"""
|
"""
|
||||||
from app.db_sql import redis_lookup_id_random
|
from app.db_sql import redis_lookup_id_random, sql_select
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
resolved_account_id = None
|
resolved_account_id = None
|
||||||
resolved_account_id_random = None
|
resolved_account_id_random = None
|
||||||
auth_method = 'guest'
|
auth_method = 'guest'
|
||||||
|
api_key_authorized = False
|
||||||
|
|
||||||
if x_account_id:
|
# 1. Mandatory Machine Auth (API Key)
|
||||||
resolved_account_id_random = x_account_id
|
# This identifies the script/app, regardless of the user/account context.
|
||||||
if looked_up_id := redis_lookup_id_random(table_name='account', record_id_random=x_account_id):
|
if x_aether_api_key:
|
||||||
resolved_account_id = looked_up_id
|
sql = "SELECT * FROM api_key WHERE (public_key = :key OR secret_key = :key) LIMIT 1"
|
||||||
auth_method = 'legacy_header'
|
if api_key_rec := sql_select(sql=sql, data={'key': x_aether_api_key}):
|
||||||
elif x_no_account_id_token:
|
if api_key_rec.get('enable'):
|
||||||
resolved_account_id_random = x_no_account_id_token
|
now = datetime.now()
|
||||||
if looked_up_id := redis_lookup_id_random(table_name='account', record_id_random=x_no_account_id_token):
|
enable_from = api_key_rec.get('enable_from')
|
||||||
resolved_account_id = looked_up_id
|
enable_to = api_key_rec.get('enable_to')
|
||||||
auth_method = 'token_query'
|
if (not enable_from or enable_from <= now) and (not enable_to or now <= enable_to):
|
||||||
elif x_no_account_id:
|
api_key_authorized = True
|
||||||
resolved_account_id = None
|
else:
|
||||||
resolved_account_id_random = '--- NO ACCOUNT ---'
|
log.warning(f"Security: API Key {x_aether_api_key} expired/not yet valid.")
|
||||||
auth_method = 'bypass'
|
else:
|
||||||
|
log.warning(f"Security: API Key {x_aether_api_key} is disabled.")
|
||||||
|
else:
|
||||||
|
log.warning(f"Security: API Key {x_aether_api_key} not found.")
|
||||||
|
|
||||||
|
# 2. Context Resolution (Only if API Key is valid)
|
||||||
|
if api_key_authorized:
|
||||||
|
# A. Resolve via Account ID Header
|
||||||
|
if x_account_id:
|
||||||
|
resolved_account_id_random = x_account_id
|
||||||
|
if looked_up_id := redis_lookup_id_random(table_name='account', record_id_random=x_account_id):
|
||||||
|
resolved_account_id = looked_up_id
|
||||||
|
auth_method = 'legacy_header'
|
||||||
|
|
||||||
|
# B. Resolve via JWT / Token Query Param
|
||||||
|
elif x_no_account_id_token:
|
||||||
|
resolved_account_id_random = x_no_account_id_token
|
||||||
|
if looked_up_id := redis_lookup_id_random(table_name='account', record_id_random=x_no_account_id_token):
|
||||||
|
resolved_account_id = looked_up_id
|
||||||
|
auth_method = 'token_query'
|
||||||
|
|
||||||
|
# C. Resolve via Administrative Bypass
|
||||||
|
elif x_no_account_id and x_no_account_id.lower() not in ['false', '0', 'null', 'undefined', 'none', 'no_account_id_here']:
|
||||||
|
resolved_account_id = None
|
||||||
|
resolved_account_id_random = '--- NO ACCOUNT ---'
|
||||||
|
auth_method = 'bypass'
|
||||||
|
|
||||||
return AccountContext(
|
return AccountContext(
|
||||||
account_id=resolved_account_id,
|
account_id=resolved_account_id,
|
||||||
account_id_random=resolved_account_id_random,
|
account_id_random=resolved_account_id_random,
|
||||||
auth_method=auth_method,
|
auth_method=auth_method,
|
||||||
administrator=(auth_method == 'bypass'),
|
administrator=(auth_method == 'bypass'),
|
||||||
@@ -51,10 +79,67 @@ def get_account_context_optional(
|
|||||||
def get_account_context(
|
def get_account_context(
|
||||||
x_account_id: Optional[str] = Header(None, min_length=11, max_length=22),
|
x_account_id: Optional[str] = Header(None, min_length=11, max_length=22),
|
||||||
x_no_account_id: Optional[str] = Header(None, min_length=3, max_length=100),
|
x_no_account_id: Optional[str] = Header(None, min_length=3, max_length=100),
|
||||||
x_no_account_id_token: Optional[str] = Query(None, min_length=11, max_length=22),
|
x_no_account_id_token: Optional[str] = Query(None, alias='jwt', min_length=11, max_length=22),
|
||||||
|
x_aether_api_key: Optional[str] = Header(None, min_length=11, max_length=22),
|
||||||
) -> AccountContext:
|
) -> AccountContext:
|
||||||
"""Strict version of account context resolution."""
|
"""Strict version of account context resolution."""
|
||||||
ctx = get_account_context_optional(x_account_id, x_no_account_id, x_no_account_id_token)
|
ctx = get_account_context_optional(x_account_id, x_no_account_id, x_no_account_id_token, x_aether_api_key)
|
||||||
|
if ctx.auth_method == 'guest':
|
||||||
|
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail='Account context required.')
|
||||||
|
return ctx
|
||||||
|
|
||||||
|
|
||||||
|
# --- Shared Pagination & Status Dependencies ---
|
||||||
|
|
||||||
|
class PaginationParams:
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
limit: int = Query(100, ge=0),
|
||||||
|
offset: int = Query(0, ge=0),
|
||||||
|
):
|
||||||
|
self.limit = limit
|
||||||
|
self.offset = offset
|
||||||
|
|
||||||
|
class StatusFilterParams:
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
enabled: str = Query('enabled'),
|
||||||
|
hidden: str = Query('not_hidden'),
|
||||||
|
):
|
||||||
|
self.enabled = enabled
|
||||||
|
self.hidden = hidden
|
||||||
|
|
||||||
|
class SerializationParams:
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
by_alias: bool = Query(True),
|
||||||
|
exclude_unset: bool = Query(False),
|
||||||
|
exclude_defaults: bool = Query(False),
|
||||||
|
exclude_none: bool = Query(False),
|
||||||
|
):
|
||||||
|
self.by_alias = by_alias
|
||||||
|
self.exclude_unset = exclude_unset
|
||||||
|
self.exclude_defaults = exclude_defaults
|
||||||
|
self.exclude_none = exclude_none
|
||||||
|
|
||||||
|
class DelayParams:
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
x_delay_ms: Optional[int] = Header(0, alias='X-Delay-ms'),
|
||||||
|
delay_ms: Optional[int] = Query(0),
|
||||||
|
):
|
||||||
|
val = max(x_delay_ms or 0, delay_ms or 0)
|
||||||
|
self.sleep_time_ms = val
|
||||||
|
self.sleep_time_s = val / 1000.0
|
||||||
|
|
||||||
|
def get_account_context(
|
||||||
|
x_account_id: Optional[str] = Header(None, min_length=11, max_length=22),
|
||||||
|
x_no_account_id: Optional[str] = Header(None, min_length=3, max_length=100),
|
||||||
|
x_no_account_id_token: Optional[str] = Query(None, alias='jwt', min_length=11, max_length=22),
|
||||||
|
x_aether_api_key: Optional[str] = Header(None, min_length=11, max_length=22),
|
||||||
|
) -> AccountContext:
|
||||||
|
"""Strict version of account context resolution."""
|
||||||
|
ctx = get_account_context_optional(x_account_id, x_no_account_id, x_no_account_id_token, x_aether_api_key)
|
||||||
if ctx.auth_method == 'guest':
|
if ctx.auth_method == 'guest':
|
||||||
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail='Account context required.')
|
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail='Account context required.')
|
||||||
return ctx
|
return ctx
|
||||||
|
|||||||
59
documentation/PROJECT_SECURITY_HARDENING.md
Normal file
59
documentation/PROJECT_SECURITY_HARDENING.md
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
# Project: API Security Hardening (V3)
|
||||||
|
|
||||||
|
**Status:** Draft / Planning
|
||||||
|
**Date:** Jan 18, 2026
|
||||||
|
**Owner:** Scott / Aether API Team
|
||||||
|
|
||||||
|
## 1. Executive Summary
|
||||||
|
This project aims to close a critical security vulnerability in the Aether API V3 dependencies where the `x_no_account_id` header allows unauthorized "Superuser/Bypass" access without validation. Additionally, it addresses the lack of cryptographic verification for JWTs in the V3 CRUD layer.
|
||||||
|
|
||||||
|
## 2. The Vulnerabilities
|
||||||
|
|
||||||
|
### A. The "Bypass Header" (Critical)
|
||||||
|
* **Issue:** The `get_account_context_optional` dependency in `app/routers/dependencies_v3.py` accepts a header `x_no_account_id`. If present, it sets `auth_method = 'bypass'` and grants full administrative privileges (`super=True`).
|
||||||
|
* **Note:** This header may be sent as `x_no_account_id` or `x-no-account-id`. FastAPI's `Header` logic should handle the conversion, but validation must be explicit.
|
||||||
|
* **Current State:** There is **no** validation of this header. Sending `x_no_account_id: anything` works.
|
||||||
|
* **Risk:** Total system compromise via simple header injection.
|
||||||
|
|
||||||
|
### B. JWT Verification (High)
|
||||||
|
* **Issue:** The system validates users by looking up the `x_account_id` string in the database (Redis/MariaDB).
|
||||||
|
* **Current State:** It does **not** cryptographically verify the JWT signature or expiration in the V3 dependency chain.
|
||||||
|
* **Risk:** Account impersonation if an Account ID is leaked.
|
||||||
|
|
||||||
|
## 3. Implementation Plan
|
||||||
|
|
||||||
|
### Phase 1: Secure the Bypass Header (Immediate)
|
||||||
|
**Goal:** Restrict "Bypass Mode" to clients possessing a valid, active API Key.
|
||||||
|
|
||||||
|
1. **Refactor Dependencies:**
|
||||||
|
* Update `get_account_context_optional` to accept `x_aether_api_key`.
|
||||||
|
* **Logic Change:** If `x_no_account_id` header is detected:
|
||||||
|
* **Require** `x_aether_api_key`.
|
||||||
|
* **Lookup** key in `api_key` table.
|
||||||
|
* **Verify** `enable = 1` and `now()` is between `enable_from` and `enable_to`.
|
||||||
|
* **Failure Behavior:** If key is invalid/missing, log warning and deny 'bypass' status (fall back to 'guest').
|
||||||
|
|
||||||
|
2. **Verification:**
|
||||||
|
* Test `curl` with header only -> Expect 403/Forbidden.
|
||||||
|
* Test `curl` with header + valid Key -> Expect 200/OK.
|
||||||
|
* Test Frontend File Download (uses `x_no_account_id_token` param) -> Expect 200/OK (No regression).
|
||||||
|
|
||||||
|
### Phase 2: JWT Verification (Subsequent)
|
||||||
|
**Goal:** Replace DB-lookup auth with cryptographic JWT validation.
|
||||||
|
|
||||||
|
1. **Audit:** Confirm `app/lib_jwt.py` logic is sound.
|
||||||
|
2. **Middleware/Dependency:** Integrate `decode_jwt` into the `get_account_context` flow.
|
||||||
|
3. **Transition:** Allow dual-mode (DB lookup OR JWT) for a transition period if necessary, or cut over if frontend sends valid JWTs.
|
||||||
|
|
||||||
|
## 4. Impact Analysis
|
||||||
|
* **Frontend (SvelteKit):**
|
||||||
|
* Audit confirmed no usage of `x_no_account_id` **header** in active source code.
|
||||||
|
* Usage of `x_no_account_id_token` (Query Param) is **safe** and distinct from the header logic.
|
||||||
|
* **Scripts/External Tools:**
|
||||||
|
* Any external scripts using the "Bypass" header must be updated to send a valid API Key.
|
||||||
|
|
||||||
|
## 5. Action Items
|
||||||
|
- [ ] Create `PROJECT_SECURITY_HARDENING.md` (This document).
|
||||||
|
- [ ] Refactor `app/routers/dependencies_v3.py`.
|
||||||
|
- [ ] Verify fix with `curl` tests.
|
||||||
|
- [ ] Commit changes.
|
||||||
@@ -19,30 +19,38 @@ This guide explains how to update or create frontend functions to interact with
|
|||||||
|
|
||||||
## 2. Authentication and Security (Mandatory in V3)
|
## 2. Authentication and Security (Mandatory in V3)
|
||||||
|
|
||||||
As of January 2026, the V3 architecture enforces strict **Multi-Tenant Isolation**. Most requests now require valid authentication to prevent data leakage between accounts.
|
As of January 2026, the V3 architecture enforces strict **Multi-Tenant Isolation** and **Machine Authorization**. Most requests now require two levels of validation.
|
||||||
|
|
||||||
### A. Authentication Requirement
|
### A. The "Entry Ticket" (API Key)
|
||||||
Almost all V3 CRUD endpoints require a standard Bearer token in the `Authorization` header.
|
**Mandatory for all requests.** To prevent unauthorized clients from using the API, every request must provide a valid `x-aether-api-key` in the header. This identifies the application or script (e.g., the Svelte frontend, the Flask legacy app, or an AI agent).
|
||||||
|
|
||||||
* **Mandatory:** You must provide a valid JWT for nearly all requests.
|
* **Header:** `x-aether-api-key: <your_app_key>`
|
||||||
* **Account Isolation:** The backend automatically filters all results based on the `account_id` found in your JWT. You cannot access data belonging to another account even if you know the random ID.
|
* **Status Code:** `403 Forbidden` if missing, invalid, or expired.
|
||||||
* **Status Codes:**
|
|
||||||
* `401 Unauthorized`: Your JWT is invalid or expired.
|
|
||||||
* `403 Forbidden`: No authentication provided, or you attempted to access an object belonging to a different account.
|
|
||||||
|
|
||||||
**Example Request Header:**
|
### B. The "Visa" (Account Context)
|
||||||
```http
|
Once the API Key is validated, you must specify the context of your request.
|
||||||
Authorization: Bearer <your_jwt_token>
|
|
||||||
```
|
|
||||||
|
|
||||||
### B. The "Bootstrap Paradox" Exception (`site_domain`)
|
1. **Standard User Access**: Provide the `x-account-id` (the random string ID). This restricts all results to that specific tenant.
|
||||||
|
* **Header:** `x-account-id: <account_id_random>`
|
||||||
|
2. **Administrative Bypass**: For authorized scripts needing global access, use the bypass header.
|
||||||
|
* **Header:** `x-no-account-id: bypass`
|
||||||
|
3. **Authentication Requirement**: Most V3 CRUD endpoints also require a standard Bearer token in the `Authorization` header for user-level actions.
|
||||||
|
|
||||||
|
### C. The "Bootstrap Paradox" Exception (`site_domain`)
|
||||||
There is one critical exception to strict authentication: **`site_domain` search**.
|
There is one critical exception to strict authentication: **`site_domain` search**.
|
||||||
|
|
||||||
Because the frontend needs to lookup the site configuration (to know which account it's on) *before* a user can log in, the following endpoint allows unauthenticated (guest) access:
|
Because the frontend needs to lookup the site configuration (to know which account it's on) *before* it has an Account ID or User token, the following endpoint allows unauthenticated (**guest**) access:
|
||||||
|
|
||||||
**Endpoint:** `POST /v3/crud/site_domain/search`
|
**Endpoint:** `POST /v3/crud/site_domain/search`
|
||||||
|
|
||||||
This is the only V3 search allowed without a JWT. All other object types (journal, account, post, etc.) will return `403 Forbidden` if accessed without a token.
|
This is the only V3 search allowed without an Account ID or JWT. However, it **still requires a valid API Key**.
|
||||||
|
|
||||||
|
### D. Fail-Fast on Auth Failures (401/403)
|
||||||
|
To prevent unnecessary server load and looping, the frontend implements a **Fail-Fast** policy for authentication and authorization errors.
|
||||||
|
|
||||||
|
* **401 Unauthorized**: Means the token is missing, invalid, or expired.
|
||||||
|
* **403 Forbidden**: Means the token is valid, but you do not have permission to access the specific resource (or you attempted a cross-tenant request without bypass).
|
||||||
|
* **Protocol**: If the API returns a 401 or 403, the frontend **must not retry** the request automatically. It should stop immediately and, if applicable, redirect the user to the login page or display a "Permission Denied" message.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -185,6 +193,72 @@ const downloadUrl = `${BASE_URL}/v3/crud/hosted_file/${fileId}/?jwt=${jwtToken}`
|
|||||||
|
|
||||||
## 7. Best Practices for V3
|
## 7. Best Practices for V3
|
||||||
|
|
||||||
1. **Use `view` for Rich Data**: Instead of manually joining data in separate calls, use `?view=enriched` or `?view=detail`.
|
---
|
||||||
2. **Singular Nouns**: Always use singular names for `obj_type` (e.g., `journal`).
|
|
||||||
3. **Strict Typing**: Ensure your `data` objects match the backend models to avoid `400 Bad Request` validation errors.
|
|
||||||
|
|
||||||
|
## 8. Standard Patterns & Common Pitfalls (2026 Update)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
To ensure stability across the Aether mesh, all frontend components must adhere to these established patterns.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
### A. The "Whitelist" Standard for Saves
|
||||||
|
|
||||||
|
**Problem:** Sending the entire object returned by a GET request back to a PATCH endpoint will cause a `400 Bad Request`. This happens because the object contains technical metadata (like `journal_id`, `created_on`, `for_type`) that the API cannot "SET".
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
**Standard:** When preparing a save payload, explicitly whitelist ONLY the user-editable fields.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
```ts
|
||||||
|
|
||||||
|
// ✅ CORRECT: Whitelist only editable fields
|
||||||
|
|
||||||
|
const data_kv = {
|
||||||
|
|
||||||
|
name: tmp_obj.name,
|
||||||
|
|
||||||
|
content: tmp_obj.content,
|
||||||
|
|
||||||
|
tags: tmp_obj.tags,
|
||||||
|
|
||||||
|
category_code: tmp_obj.category_code
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// ❌ WRONG: Passing the whole object or just blacklisting a few
|
||||||
|
|
||||||
|
const data_kv = { ...tmp_obj };
|
||||||
|
|
||||||
|
delete data_kv.id; // Still contains other computed columns!
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
### B. The Triple-ID Save Pattern
|
||||||
|
|
||||||
|
**Standard:** When sending an ID in a POST/PATCH payload (e.g. creating a child object), use the random string version with the `_id_random` suffix.
|
||||||
|
|
||||||
|
* **Property:** `[obj_type]_id_random`
|
||||||
|
|
||||||
|
* **Value:** A string (e.g., `qpgvOh5nOYI`)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
**Warning:** Sending a string value under an integer key (e.g. `journal_id: "qpgvOh5nOYI"`) will trigger a Pydantic validation error on the backend.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
### C. Handle 'NULL' vs '0' for Booleans
|
||||||
|
|
||||||
|
**Pitfall:** Fields like `hide` or `priority` can be `NULL` in the database.
|
||||||
|
|
||||||
|
**Standard:** Ensure your synchronization logic in components handles `null`, `undefined`, and `0` identically for boolean checks to prevent "ghost" changes being detected by Svelte 5.
|
||||||
52
tests/repro_security_bypass.py
Normal file
52
tests/repro_security_bypass.py
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
import requests
|
||||||
|
import sys
|
||||||
|
|
||||||
|
BASE_URL = "http://dev-api.oneskyit.com" # Standard Dev Domain
|
||||||
|
OBJ_TYPE = "journal"
|
||||||
|
ENDPOINT = f"{BASE_URL}/v3/crud/{OBJ_TYPE}/schema"
|
||||||
|
|
||||||
|
# Replace with the key found from DB query
|
||||||
|
VALID_API_KEY = "dummy_key_placeholder"
|
||||||
|
|
||||||
|
def test_request(description, headers, expected_status):
|
||||||
|
print(f"Testing: {description}")
|
||||||
|
try:
|
||||||
|
response = requests.get(ENDPOINT, headers=headers, timeout=10)
|
||||||
|
status = response.status_code
|
||||||
|
result = "PASS" if status == expected_status else "FAIL"
|
||||||
|
print(f" Status: {status} (Expected: {expected_status}) -> {result}")
|
||||||
|
if result == "FAIL":
|
||||||
|
print(f" Response: {response.text[:200]}...")
|
||||||
|
return result == "PASS"
|
||||||
|
except Exception as e:
|
||||||
|
print(f" Error: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
def main():
|
||||||
|
if len(sys.argv) > 1:
|
||||||
|
global VALID_API_KEY
|
||||||
|
VALID_API_KEY = sys.argv[1]
|
||||||
|
|
||||||
|
print(f"--- Security Bypass Test (Target: {ENDPOINT}) ---")
|
||||||
|
|
||||||
|
# Case 1: No Auth
|
||||||
|
# Expected: 403 Forbidden (Account context required)
|
||||||
|
test_request("No Auth Headers", {}, 403)
|
||||||
|
|
||||||
|
# Case 2: Vulnerability Check (Bypass Header Only)
|
||||||
|
# AFTER FIX: Expected 403 (Protected)
|
||||||
|
test_request("Bypass Header Only (Vulnerability Check)", {"x-no-account-id": "bypass"}, 403)
|
||||||
|
|
||||||
|
# Case 3: Invalid API Key + Bypass Header
|
||||||
|
# Expected: 403 Forbidden
|
||||||
|
test_request("Bypass Header + Invalid Key", {"x-no-account-id": "bypass", "x-aether-api-key": "invalid-key-12345"}, 403)
|
||||||
|
|
||||||
|
# Case 4: Valid API Key + Bypass Header
|
||||||
|
# Expected: 200 OK
|
||||||
|
if VALID_API_KEY != "dummy_key_placeholder":
|
||||||
|
test_request("Bypass Header + Valid Key", {"x-no-account-id": "bypass", "x-aether-api-key": VALID_API_KEY}, 200)
|
||||||
|
else:
|
||||||
|
print("Skipping Case 4 (No Valid API Key provided)")
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
Reference in New Issue
Block a user