Skip to content

Instantly share code, notes, and snippets.

@justinbarry
Created March 3, 2026 21:58
Show Gist options
  • Select an option

  • Save justinbarry/0412b5853a1312cd60168dd89e9909c9 to your computer and use it in GitHub Desktop.

Select an option

Save justinbarry/0412b5853a1312cd60168dd89e9909c9 to your computer and use it in GitHub Desktop.
Security Audit: oauth3 + Redacted — Claude Opus 4 vs Gemini 2.5 Pro vs Codex GPT-5.3 (2026-03-03)

Now I have all the data verified. Here is the complete security audit report:


Security Audit Report: Redacted Platform

Date: 2026-03-03
Scope: oauth3/ (Rust/Axum TEE authorization server) + Redacted/ (Next.js + CosmWasm frontend)
Auditor: Claude Code (Automated Static Analysis)


Executive Summary

The Redacted platform combines a TEE-hosted OAuth 2.0 authorization server (oauth3) with a Next.js frontend (Redacted) and CosmWasm smart contracts on the XION blockchain to issue clearance badges based on Gmail content verification. The architecture is ambitious and generally well-designed, with strong cryptographic foundations (PKCE, HMAC sessions, encrypted cookies, TDX attestation, secp256r1 signature verification).

However, 2 critical, 4 high, 6 medium, 3 low, and 4 informational findings were identified. The most severe issues are an open-redirect default in is_trusted_redirect() and a placeholder OAuth mode that creates authenticated sessions without any identity verification. Both are in the oauth3 backend and could undermine the entire trust model.

Overall Risk Assessment: High — The critical findings in the oauth3 server could allow attackers to bypass authentication and redirect users to malicious origins. The smart contract and frontend are more robust, but the TEE attestation chain is only as strong as its weakest link.


Findings by Severity

Critical


SEC-01: Open Redirect Default When ALLOWED_RETURN_TO_ORIGINS Is Unset

Field Value
Severity Critical
Location oauth3/src/auth/oidc.rs:111-117

Description:
The is_trusted_redirect() function defaults to returning true (allow) when the ALLOWED_RETURN_TO_ORIGINS environment variable is not configured. This means any return_to URL will be accepted, enabling open redirect attacks.

// No allowlist configured — warn and allow for backwards compatibility
tracing::warn!(
    return_to = %url,
    "No ALLOWED_RETURN_TO_ORIGINS configured; allowing external redirect. \
     Set ALLOWED_RETURN_TO_ORIGINS to restrict."
);
true  // <-- ALLOWS ANY ORIGIN

Impact:
An attacker can craft a login URL with a malicious return_to parameter, causing the server to redirect authenticated users (with their HMAC session tokens) to an attacker-controlled domain. This enables session token theft and full account takeover. Since the HMAC token is appended to the redirect URL for trusted origins, the token would be delivered directly to the attacker.

Recommendation:
Default to false (deny) when no allowlist is configured. Fail closed, not open:

// No allowlist configured — deny by default
tracing::warn!("No ALLOWED_RETURN_TO_ORIGINS configured; rejecting external redirect.");
false

SEC-02: Placeholder OAuth Mode Creates Sessions Without Authentication

Field Value
Severity Critical
Location oauth3/src/auth/oidc.rs:244-273

Description:
When the OIDC callback receives no authorization code, state, or session cookie (indicating a failed or absent OAuth flow), the handler falls through to a "placeholder" code path that creates a new user with a fabricated identity and issues a fully authenticated 60-minute session — with zero authentication performed.

// Placeholder behavior — no OAuth actually happened
let user_id = uuid::Uuid::new_v4().to_string();
session::set_session(&cookies, &state.cookie_key, &user_id, 60);
redirect_after_login(&cookies, &state.config, &user_id, "/").into_response()

Impact:
Any unauthenticated request to the OIDC callback endpoint (e.g., GET /oauth3/auth/google/callback with no parameters) will receive a valid session cookie. An attacker can trivially obtain an authenticated session, potentially bypassing the entire OAuth flow and gaining access to the Gmail verification pipeline.

Recommendation:
Remove the placeholder fallback entirely or gate it behind a compile-time feature flag that is disabled in release builds:

#[cfg(feature = "dev-placeholder")]
{
    // ... placeholder logic, only in dev
}
#[cfg(not(feature = "dev-placeholder"))]
{
    return (StatusCode::BAD_REQUEST, "Missing authorization code").into_response();
}

High


SEC-03: API Key Generation Uses Weak RNG

Field Value
Severity High
Location oauth3/src/web/handlers/account.rs:224-238

Description:
The generate_api_key() function uses rand::thread_rng(), a non-cryptographic PRNG, instead of OsRng (the OS-provided CSPRNG). All other security-sensitive random generation in the codebase correctly uses OsRng.

fn generate_api_key() -> String {
    use rand::Rng;
    let mut rng = rand::thread_rng();  // NOT cryptographically secure
    let key: String = (0..KEY_LEN)
        .map(|_| {
            let idx = rng.gen_range(0..CHARSET.len());
            CHARSET[idx] as char
        })
        .collect();
    format!("oak_{}", key)
}

Impact:
thread_rng() may be predictable if an attacker can observe or influence system state. Generated API keys could be guessable, enabling unauthorized access to the proxy and any scoped operations.

Recommendation:
Replace with OsRng, consistent with the rest of the codebase:

use rand::rngs::OsRng;
let mut rng = OsRng;

SEC-04: Token Expiry Check Fails Open on Parse Error

Field Value
Severity High
Location oauth3/src/web/session.rs:250-258

Description:
The is_expired() function returns false (not expired) when it cannot parse the timestamp string. This means malformed or corrupted expiry timestamps will be treated as valid, never-expiring tokens.

fn is_expired(timestamp: &str) -> bool {
    if let Ok(expires) = time::OffsetDateTime::parse(
        timestamp,
        &time::format_description::well_known::Rfc3339,
    ) {
        return time::OffsetDateTime::now_utc() > expires;
    }
    false  // <-- Unparseable = "not expired" (FAIL OPEN)
}

Notably, a separate is_expired function in oauth3/src/web/proxy.rs:150-151 correctly fails closed (return true on parse error), making this inconsistency even more concerning.

Impact:
If a database corruption or migration issue causes malformed timestamps in access/refresh tokens, those tokens will never expire. An attacker who obtains such a token would have indefinite access.

Recommendation:
Fail closed — treat unparseable timestamps as expired:

false  // Change to:
true   // Unparseable → treat as expired (fail closed)

SEC-05: No CORS Configuration on oauth3 Server

Field Value
Severity High
Location oauth3/src/app.rs (entire file — no CorsLayer present)

Description:
The oauth3 Axum server does not configure any CORS middleware. No tower_http::cors::CorsLayer is applied to the router. The browser's default same-origin policy will block cross-origin fetch() requests, but this is an implicit rather than explicit security control.

Impact:
Without explicit CORS headers, the server relies entirely on browser defaults. If the deployment places oauth3 behind a reverse proxy that adds permissive CORS headers, or if the server is accessed via non-browser clients, there is no server-side enforcement. Additionally, the lack of explicit configuration means simple requests (form POSTs, navigation) are not restricted.

Recommendation:
Add an explicit CorsLayer with a strict allowlist:

use tower_http::cors::{CorsLayer, AllowOrigin};
let cors = CorsLayer::new()
    .allow_origin(AllowOrigin::list(["https://theredactedfile.com".parse().unwrap()]))
    .allow_methods([Method::GET, Method::POST])
    .allow_credentials(true);
app.layer(cors);

SEC-06: No Rate Limiting on oauth3 Authentication Endpoints

Field Value
Severity High
Location oauth3/src/app.rs (entire file — no rate-limit middleware)

Description:
The oauth3 server has no rate limiting on any endpoint, including authentication-critical paths: /oauth3/auth/* (login/callback), /oauth3/token (token exchange), /verify/gmail (verification), and /api/keys (API key management). While the Next.js frontend has best-effort memory-based rate limiting, direct requests to oauth3 bypass it entirely.

Impact:
Attackers can perform credential stuffing, brute-force API keys, or flood the Gmail verification endpoint (which consumes Google API quota and TEE resources). The /oauth3/token endpoint is especially vulnerable to authorization code brute-forcing.

Recommendation:
Add rate limiting middleware, especially on token and verification endpoints:

use tower_governor::{GovernorLayer, GovernorConfigBuilder};
let config = GovernorConfigBuilder::default()
    .per_second(2)
    .burst_size(10)
    .finish()
    .unwrap();
app.layer(GovernorLayer { config });

Medium


SEC-07: Session Cookies Bypass All Proxy Scope Checks

Field Value
Severity Medium
Location oauth3/src/web/proxy.rs:156-159

Description:
The scope_allows_provider() function treats scopes: None as a wildcard, granting access to all providers. Session-based authentication sets scopes to None, meaning any logged-in user can proxy requests to any configured OAuth provider without restriction.

fn scope_allows_provider(scopes: Option<&str>, provider: &str) -> bool {
    let Some(scopes) = scopes else {
        return true;  // No scopes = allow everything
    };
    // ...
}

Impact:
A user with a valid session cookie can access any provider's API through the proxy, even providers they haven't explicitly authorized. This could enable unauthorized access to Google, GitHub, or other provider APIs through the proxy.

Recommendation:
Require explicit proxy scope for session-based access, or validate that the user has linked the requested provider:

let Some(scopes) = scopes else {
    // Session auth: verify user has linked this provider
    return false; // Or check identity table
};

SEC-08: Client Secret Comparison Not Constant-Time

Field Value
Severity Medium
Location oauth3/src/web/handlers/oauth.rs:515-516

Description:
The client secret verification compares SHA-256 hash strings using standard string equality (!=), which is susceptible to timing side-channel attacks. While the comparison is on hashes (not raw secrets), timing differences in string comparison could leak information about the hash value.

let provided_hash = hash_token(&secret);
if provided_hash != expected {  // NOT constant-time
    return Err(token_error_response(StatusCode::UNAUTHORIZED, "invalid_client"));
}

Impact:
An attacker making many requests could theoretically determine the hash value byte-by-byte through response timing analysis. This is mitigated by the hashing step (the leaked information is about the hash, not the original secret) and by network jitter, but the risk exists in principle.

Recommendation:
Use constant-time comparison:

use subtle::ConstantTimeEq;
if provided_hash.as_bytes().ct_eq(expected.as_bytes()).unwrap_u8() != 1 {
    return Err(...);
}

SEC-09: return_to Cookie Not Encrypted

Field Value
Severity Medium
Location oauth3/src/auth/session.rs:52-59

Description:
The set_login_return_to() function stores the return_to URL in a plain cookie using cookies.add(), while all other sensitive cookies in the application use cookies.private() (encrypted + signed). This means the redirect URL is visible and modifiable by the client.

pub fn set_login_return_to(cookies: &Cookies, path: &str) {
    // ...
    cookies.add(cookie);  // NOT encrypted — uses cookies.add() instead of cookies.private().add()
}

Impact:
A man-in-the-middle (when APP_FORCE_SECURE is not set) or a client-side attacker could modify the return_to cookie to redirect the user to a malicious URL after login. Combined with SEC-01 (if allowlist is not configured), this amplifies the open redirect risk.

Recommendation:
Use the private (encrypted) cookie jar:

cookies.private(key).add(cookie);

SEC-10: Smart Contract Only Validates First 32 Bytes of REPORTDATA

Field Value
Severity Medium
Location Redacted/contracts/clearance/src/contract.rs (REPORTDATA validation logic)

Description:
The TDX attestation quote contains a 64-byte REPORTDATA field. The smart contract only validates the first 32 bytes (comparing them against the SHA-256 hash of the canonical result). The remaining 32 bytes are not checked, which means they could contain arbitrary data without invalidating the quote.

Impact:
While the first 32 bytes bind the attestation to the verification result, the unchecked 32 bytes could theoretically be used to smuggle additional data through the attestation pipeline, or a future vulnerability in quote generation could exploit this unused space. The practical risk is low given the current single-purpose design.

Recommendation:
Verify that the second 32 bytes are zero-padded, matching the TEE's expected behavior:

// After checking first 32 bytes match the hash:
if report_data[32..64] != [0u8; 32] {
    return Err(ContractError::InvalidQuote("unexpected REPORTDATA padding".into()));
}

SEC-11: OAuth Callback Uses script-src 'unsafe-inline' CSP

Field Value
Severity Medium
Location Redacted/src/app/api/oauth3/callback/route.ts:39

Description:
The OAuth callback response serves an HTML page with Content-Security-Policy: default-src 'none'; script-src 'unsafe-inline'. While default-src 'none' is restrictive, script-src 'unsafe-inline' allows execution of any inline script, defeating script-based CSP protections on this page.

Impact:
If an attacker can inject content into this HTML response (e.g., through a reflected parameter — currently mitigated by the token regex validation), they could execute arbitrary JavaScript. The callback page handles the OAuth token, so XSS here could steal the token before it's stored in the httpOnly cookie.

Recommendation:
Use a nonce-based CSP:

const nonce = crypto.randomUUID();
// CSP: script-src 'nonce-${nonce}'
// <script nonce="${nonce}">...</script>

SEC-12: SystemTime::now().unwrap() Can Panic in TEE

Field Value
Severity Medium
Location oauth3/src/web/handlers/verify.rs:147-150

Description:
The verification handler calls SystemTime::now().duration_since(UNIX_EPOCH).unwrap(). While SystemTime::now() succeeding is almost universal, duration_since(UNIX_EPOCH) will panic if the system clock is before January 1, 1970. In a TEE environment where the clock is managed by the hypervisor, clock anomalies are more plausible than on standard hardware.

let timestamp = SystemTime::now()
    .duration_since(UNIX_EPOCH)
    .unwrap()  // PANICS if clock < UNIX_EPOCH
    .as_secs();

Impact:
A TEE clock anomaly would crash the verification handler, causing a denial of service for all verification requests. This is the only path to generate attestation quotes, so the entire verification pipeline would be unavailable.

Recommendation:
Use unwrap_or with a fallback or return an error:

let timestamp = SystemTime::now()
    .duration_since(UNIX_EPOCH)
    .map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "clock error"))?
    .as_secs();

Low


SEC-13: .expect() Panic in Attestation Client Construction

Field Value
Severity Low
Location oauth3/src/attestation/mod.rs:12

Description:
The get_dstack_client() function uses .expect("Failed to build Unix socket client") which will panic (crash the entire server) if the Unix socket client cannot be constructed. This only applies to the production path when DSTACK_SOCKET is set.

Impact:
If the Unix socket path is invalid or inaccessible at startup, the server will crash. This is a low-severity availability issue since it would be caught immediately during deployment.

Recommendation:
Return Result and handle the error gracefully:

fn get_dstack_client() -> Result<reqwest::Client> {
    // ...
    .build().context("Failed to build Unix socket client")?
}

SEC-14: Minimal Global Content Security Policy

Field Value
Severity Low
Location Redacted/next.config.js:33

Description:
The global CSP only defines frame-ancestors 'self'. It does not restrict script-src, style-src, connect-src, img-src, or other directives. This provides clickjacking protection but no XSS mitigation at the CSP layer.

Impact:
If an XSS vulnerability exists elsewhere in the application, there is no CSP defense-in-depth to prevent script execution. The application relies solely on React's built-in escaping and input validation.

Recommendation:
Add a more comprehensive CSP. This requires careful testing with the Abstraxion SDK and Reclaim iframe:

"default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' https://s3.amazonaws.com; connect-src 'self' https://*.burnt.com https://*.phala.network; frame-ancestors 'self'"

SEC-15: Next.js Rate Limiting Is Memory-Based and Non-Persistent

Field Value
Severity Low
Location Redacted/src/middleware.ts

Description:
The rate limiting in the Next.js middleware uses an in-memory Map that resets on every cold start (acknowledged in code comments). On serverless platforms like Cloudflare Workers (the deployment target per package.json), each worker instance has its own memory space, and cold starts are frequent.

Impact:
Rate limiting provides minimal protection in production. An attacker can reset rate limits by waiting for a cold start or by hitting different worker instances. The oauth3 server (SEC-06) has no rate limiting at all, so even effective frontend rate limiting could be bypassed by calling oauth3 directly.

Recommendation:
Use a distributed rate limiter (e.g., Cloudflare KV, Upstash Redis) for persistent enforcement, or implement rate limiting at the oauth3 layer where it cannot be bypassed.


Informational


SEC-16: Hardcoded Suspect Email Address

Field Value
Severity Informational
Location oauth3/src/web/handlers/verify.rs:13, Redacted/src/app/api/oauth3/verify/route.ts:57, Redacted/src/app/api/reclaim/init/route.ts:26

Description:
The email address jeevacation@gmail.com is hardcoded in three locations across both repositories. Changing the target email requires redeploying both applications.

Recommendation:
Move to a shared environment variable (e.g., SUSPECT_EMAIL) to enable configuration changes without code deployment.


SEC-17: Production CVM URL Exposed in .env.production

Field Value
Severity Informational
Location Redacted/.env.production:12-13

Description:
The production CVM URL (https://b7de194832e39104e31cbb56b0efadd9fd3466b4-8080.dstack-pha-prod5.phala.network) is committed to the repository in both OAUTH3_BASE_URL and NEXT_PUBLIC_OAUTH3_BASE_URL. While the CVM is designed to be publicly accessible, exposing the infrastructure URL enables direct targeting.

Recommendation:
Consider using a custom domain or CDN in front of the CVM to decouple the public URL from the infrastructure hostname.


SEC-18: Dev Cookie Key Auto-Generated If Not Configured

Field Value
Severity Informational
Location oauth3/src/config.rs

Description:
When COOKIE_KEY_BASE64 is not set, the server generates a random key at startup with a warning. This means sessions are lost on restart and cannot be shared across multiple instances. This is acceptable for development but would be a problem if accidentally deployed without configuration.

Recommendation:
Consider making COOKIE_KEY_BASE64 a required configuration in production (fail to start if not set and not in dev mode).


SEC-19: Alpha-Version Abstraxion Dependency With Compile-Time Patching

Field Value
Severity Informational
Location Redacted/package.json:16, Redacted/abstraxion-patch-loader.js

Description:
The application depends on @burnt-labs/abstraxion@^1.0.0-alpha.73 and applies compile-time regex-based patches via a custom webpack loader to fix grant comparison bugs. This creates a fragile dependency: any update to abstraxion could silently break the patches without warning.

Recommendation:
Pin the exact version (1.0.0-alpha.73 without ^), and add build-time verification that patches were applied successfully. Track upstream for a stable release.


Cross-Repo Integration Analysis

Token Flow: Redacted Frontend → oauth3 CVM → Google → Smart Contract

The verification pipeline spans both repositories:

  1. Login (Redacted/api/oauth3/loginoauth3/auth/google): The frontend redirects users to the CVM's Google OAuth flow. The return_to URL is set via a cookie. Risk: SEC-01 (open redirect) and SEC-09 (unencrypted return_to cookie) could allow an attacker to hijack this redirect.

  2. Callback (oauth3/auth/google/callbackRedacted/api/oauth3/callback): The CVM completes OAuth, sets a session, and redirects back to the frontend with an HMAC session token. The frontend stores this as an httpOnly cookie. Risk: SEC-02 (placeholder mode) could generate a valid session without any Google authentication actually occurring.

  3. Verification (Redacted/api/oauth3/verifyoauth3/verify/gmail): The frontend proxies the verification request with the session token. The CVM searches Gmail, generates a TDX attestation quote, and returns the signed result. Risk: The proxy at Redacted/api/oauth3/verify correctly validates the address format and cleans up the token after use.

  4. Minting (Redacted/VerifyFlow.tsx → XION blockchain): The frontend submits the CVM's result and quote to the clearance smart contract, which verifies the TDX attestation and mints an NFT badge. Risk: SEC-10 (unchecked REPORTDATA bytes) is a minor gap in the contract's validation.

Trust Boundary Analysis

The critical trust boundary is between the CVM (oauth3) and the smart contract. The CVM's TDX attestation quote must convince the contract that verification happened inside a genuine TEE. The chain is:

  • CVM side: Generates CanonicalResult → SHA-256 hash → TDX REPORTDATA → signed quote
  • Contract side: Parses quote → extracts REPORTDATA → verifies hash matches result → verifies secp256r1 signature against stored TEE pubkey

This chain is sound, provided:

  1. The TEE pubkey in the contract is authentic (set by admin)
  2. The CVM code running in the TEE is the audited version (verifiable via TDX quote's MRTD/RTMR fields — not currently checked by the contract)
  3. The placeholder mode (SEC-02) is not reachable in production

Positive Observations

  1. PKCE S256 enforcement — The OAuth2 implementation correctly requires S256 PKCE for public clients and rejects the plain method (oauth3/src/web/handlers/oauth.rs).

  2. Authorization code stored as SHA-256 hash — Auth codes are hashed before storage and consumed atomically, preventing replay attacks.

  3. OIDC token validation — Full signature, issuer, audience, nonce, and expiry validation via the openidconnect crate.

  4. Google token revocation after verification — The CVM revokes the Google access token immediately after Gmail search and unlinks the Google identity from the user account (verify.rs:127-145).

  5. Encrypted session cookies — All session cookies use tower-cookies private jar (encrypted + signed), except the return_to cookie (SEC-09).

  6. Smart contract attestation verification — The CosmWasm contract performs thorough TDX v4 quote parsing, secp256r1 signature verification, REPORTDATA hash binding, address binding, 24-hour freshness enforcement, and duplicate badge prevention.

  7. Parameterized database queries — Diesel ORM is used throughout oauth3, providing inherent SQL injection protection.

  8. SSRF protection in proxy — The oauth3 proxy validates paths against traversal attempts (.., //, @) and verifies the resolved URL host matches the configured provider host.

  9. HTML escaping in consent page — The OAuth consent page uses a dedicated escape_html() function that handles all five critical characters (&, <, >, ", ').

  10. Arithmetic overflow protection — The smart contract's Cargo.toml enables overflow-checks = true in the release profile.

  11. httpOnly cookie for OAuth token — The frontend stores the CVM session token in an httpOnly cookie with SameSite=Lax, preventing XSS-based token theft.


Summary Table

ID Severity Title Location
SEC-01 Critical Open redirect default when allowlist unset oauth3/src/auth/oidc.rs:117
SEC-02 Critical Placeholder mode creates sessions without auth oauth3/src/auth/oidc.rs:244-273
SEC-03 High API key generation uses weak RNG oauth3/src/web/handlers/account.rs:229
SEC-04 High Token expiry check fails open on parse error oauth3/src/web/session.rs:250-257
SEC-05 High No CORS configuration on oauth3 server oauth3/src/app.rs
SEC-06 High No rate limiting on auth endpoints oauth3/src/app.rs
SEC-07 Medium Session cookies bypass proxy scope checks oauth3/src/web/proxy.rs:156-159
SEC-08 Medium Client secret comparison not constant-time oauth3/src/web/handlers/oauth.rs:516
SEC-09 Medium return_to cookie not encrypted oauth3/src/auth/session.rs:52-59
SEC-10 Medium Only first 32 bytes of REPORTDATA validated Redacted/contracts/clearance/src/contract.rs
SEC-11 Medium CSP uses unsafe-inline in callback Redacted/src/app/api/oauth3/callback/route.ts:39
SEC-12 Medium SystemTime::now().unwrap() can panic in TEE oauth3/src/web/handlers/verify.rs:147-150
SEC-13 Low .expect() panic in attestation client oauth3/src/attestation/mod.rs:12
SEC-14 Low Minimal global CSP (frame-ancestors only) Redacted/next.config.js:33
SEC-15 Low Memory-based rate limiting resets on cold start Redacted/src/middleware.ts
SEC-16 Info Hardcoded suspect email in 3 locations Multiple files
SEC-17 Info Production CVM URL committed to repo Redacted/.env.production
SEC-18 Info Dev cookie key auto-generated if unconfigured oauth3/src/config.rs
SEC-19 Info Alpha dependency with compile-time patches Redacted/package.json

Totals: 2 Critical, 4 High, 6 Medium, 3 Low, 4 Informational


End of report.

Security Audit Report

Executive Summary

The strongest issues are at the oauth3 ↔ Redacted handoff and in the Gmail verification path itself. The current code can mint a “clean” proof when the Gmail API call fails, and the frontend accepts arbitrary token= callback URLs without tying them to a locally initiated login. Together, those flaws are enough to break the badge’s core integrity guarantees.

The oauth3 service also exposes high-impact boundary risks: it can export bearer session tokens to arbitrary external return_to URLs by default, it forwards oauth3 cookies to upstream providers through the proxy, and it allows generic proxy-scoped API keys/app tokens to call the highly sensitive /verify/gmail route. On top of that, OAuth provider secrets and user refresh/access tokens are stored plaintext in the database.

I did not confirm SQL injection, command injection, permissive CORS, or a CosmWasm reentrancy/overflow issue from the reviewed source. This was a source audit; I did not execute live Google, Phala, or XION flows.

Critical Findings

FIND-001: Gmail API failures are treated as “clean” results

Severity: Critical

Location: verify.rs:36, verify.rs:101, oidc.rs:310, .env.example:24

Description: GmailListResponse defaults result_size_estimate to 0, and the Gmail request does not call error_for_status() before deserializing JSON. That means Gmail error bodies such as insufficient-scope 403, quota failures, or other JSON error responses can deserialize successfully and be interpreted as 0 matching messages. The repository defaults make this worse: the example Google scopes omit any Gmail scope, so a default setup is likely to hit this exact path.

Impact: users can receive a valid TDX-attested "clean": true proof without a successful inbox search. That directly undermines the badge’s integrity and can result in false on-chain clearance issuance.

Recommendation: require a Gmail scope during Google login, fail closed on non-2xx Gmail responses, and reject bodies that do not contain the expected success shape. At minimum:

let gmail_resp: GmailListResponse = http
    .get(&gmail_url)
    .bearer_auth(&access_token)
    .send()
    .await?
    .error_for_status()?
    .json()
    .await?;

Also add a startup/config validation that rejects Google provider configs missing gmail.metadata or gmail.readonly.

FIND-002: Redacted callback blindly accepts arbitrary token= URLs

Severity: Critical

Location: callback route:6, verify route:13, VerifyFlow.tsx:224, VerifyFlow.tsx:235

Description: /api/oauth3/callback stores any syntactically valid token query parameter into the oauth3_token cookie. There is no locally generated state, nonce, or login-attempt binding. /api/oauth3/verify later trusts that cookie and forwards it to oauth3 as Authorization: Bearer .... An attacker can log in with their own clean Google account, capture their own callback URL containing the bearer token, and send https://theredactedfile.com/api/oauth3/callback?token=... to a victim. If the victim already has a connected wallet, VerifyFlow can auto-run verification against the attacker’s oauth3 session and mint a badge to the victim’s address.

Impact: complete integrity break of the clearance flow. A badge can be issued for wallet A using Gmail account B.

Recommendation: do not accept bearer tokens from a GET callback URL. Replace the handoff with a one-time code tied to a locally stored login state and exchange it server-side. At minimum:

  1. Generate a local nonce/state in /api/oauth3/login and store it in a short-lived cookie.
  2. Require that state in /api/oauth3/callback.
  3. Exchange a one-time code server-to-server instead of accepting token= directly.
  4. Reject direct callback hits that were not initiated by the current browser session.

High Findings

FIND-003: oauth3 exports bearer session tokens to arbitrary external return_to URLs by default

Severity: High

Location: auth.rs:11, oidc.rs:59, oidc.rs:80

Description: oauth3 accepts a caller-supplied return_to parameter, stores it, and after login appends a bearer session token to that URL. If ALLOWED_RETURN_TO_ORIGINS is unset, is_trusted_redirect() explicitly allows any external origin “for backwards compatibility”. The token is then sent in the query string to the attacker-controlled origin.

Impact: session takeover for any deployment following repo defaults or omitting ALLOWED_RETURN_TO_ORIGINS. The stolen token can be used against /verify/gmail and proxy-backed provider APIs for its lifetime.

Recommendation: default-deny external return targets. Only allow explicitly configured origins, and prefer a one-time server-side code exchange over query-string bearer tokens. If cross-origin handoff is required, bind the token to an audience/origin and single use.

FIND-004: Proxy forwards oauth3 cookies and internal headers to upstream providers

Severity: High

Location: proxy.rs:174, proxy.rs:193, proxy.rs:211

Description: forward_request() copies nearly all inbound headers to the upstream provider. It strips only a few hop-by-hop headers. For browser-originated same-origin requests, that includes the oauth3 Cookie header carrying the encrypted sid session cookie. Upstream providers therefore receive oauth3 session material unnecessarily.

Impact: Google/GitHub/or any configured provider can observe oauth3 session cookies and other internal headers. A malicious or compromised provider could replay the cookie against oauth3 and impersonate the user for cookie-authenticated routes.

Recommendation: use an explicit header allowlist when proxying. Strip at least cookie, set-cookie, authorization from the inbound request, and avoid forwarding browser-only headers such as origin and referer unless required.

FIND-005: /verify/gmail is callable with generic proxy API keys and app access tokens

Severity: High

Location: session.rs:93, session.rs:129, session.rs:176, verify.rs:46

Description: SessionUser accepts three bearer types on any route that uses it: custom session tokens, API keys with proxy, and OAuth app access tokens with any proxy* scope. verify_gmail() uses SessionUser but performs no route-specific authorization check. That means a third-party app token or API key can invoke Gmail verification even if its consent was for ordinary proxy access, and even if the scope was for a different provider.

Impact: third-party apps can learn whether the target email appears in a user’s Gmail and can trigger disruptive side effects like token revocation and unlinking of the Google identity. This is broader access than the consent model suggests.

Recommendation: require an interactive browser session for /verify/gmail, or introduce a dedicated scope such as verify:gmail and enforce it explicitly. Do not treat generic proxy scopes as sufficient for this route.

FIND-006: OAuth provider secrets and user provider tokens are stored plaintext in the database

Severity: High

Location: schema.rs:84, schema.rs:115, 0001_init/up.sql:12, oidc.rs:418, oidc.rs:446

Description: oauth_providers.client_secret, user_identities.access_token, and user_identities.refresh_token are stored raw. The TEE protects the application runtime, but the database may be external to the TEE boundary. A database compromise bypasses the platform’s main confidentiality claim.

Impact: exposure of Google/GitHub refresh tokens and provider client secrets enables long-lived API access and account abuse outside the attested runtime.

Recommendation: encrypt provider/client secrets and user OAuth tokens before writing them to the database, using a key derived inside the TEE or a KMS policy bound to attestation. DstackClient::derive_key() already exists and is a natural fit for envelope encryption.

Medium Findings

FIND-007: proxy:{provider}:read|write scopes are accepted but never enforced

Severity: Medium

Location: apps.rs:117, apps.rs:446, proxy.rs:37, proxy.rs:156

Description: application creation and consent flows accept scopes such as proxy:google:read and proxy:google:write, but proxy authorization only checks whether the scope starts with proxy:{provider}. It never maps read vs write to HTTP methods or endpoint classes.

Impact: a token that appears read-only can still perform writes, deletes, or other mutating upstream operations if the linked provider token itself allows them.

Recommendation: until method/path restrictions exist, reject :read and :write at validation time. If you keep them, enforce them in proxy_request() based on HTTP method and, ideally, an endpoint policy map.

FIND-008: Redirect URI registration allows arbitrary plaintext http:// callbacks

Severity: Medium

Location: apps.rs:239, apps.rs:481

Description: validate_redirect_uri() accepts any http or https URI as long as it has no fragment. That permits non-loopback plaintext redirect URIs for OAuth clients.

Impact: authorization codes and state values can be exposed over cleartext or through hostile reverse proxies when apps register insecure callbacks.

Recommendation: require HTTPS for all non-loopback redirects. Allow http://localhost / 127.0.0.1 only for native-app loopback cases, and reject userinfo-bearing URLs.

FIND-009: Secure cookie protection depends on a manual APP_FORCE_SECURE flag

Severity: Medium

Location: session.rs:35, session.rs:53, session.rs:72, oidc.rs:122, account.rs:75, .env.example:16

Description: session, return-to, OIDC temporary-state, and link-provider cookies are only marked Secure when APP_FORCE_SECURE is set. The example config defaults it to false, and the code does not infer HTTPS from APP_PUBLIC_URL or proxy headers.

Impact: a production deployment behind TLS can silently emit non-Secure auth cookies if the operator forgets one environment variable, increasing session theft risk on downgraded/internal hops.

Recommendation: make Secure the default whenever APP_PUBLIC_URL is https://... or the request arrived with X-Forwarded-Proto=https. Only relax it automatically for explicit localhost development.

FIND-010: next 14.2.29 is below published security fixes, and the app uses affected image features

Severity: Medium

Location: package.json:15, next.config.js:7, ProductCard.tsx:112

Description: as of March 3, 2026, next 14.2.29 is in published advisory ranges fixed later, including image/content-disposition issues. This app uses next/image with remote S3 images, so the affected surface is present.

Impact: dependency-level vulnerabilities in the image pipeline can expose users to response/header manipulation or related content handling issues if an attacker can influence remote image responses.

Recommendation: upgrade next to a patched release in the latest supported line before further deployment. External references: GHSA-xv57-4mr9-wg8v, GHSA-g5qg-72qw-gw5v.

Low Findings

FIND-011: Proxy and attestation paths buffer entire responses in memory and are not server-throttled

Severity: Low

Location: proxy.rs:198, proxy.rs:231, middleware.rs:48, app.rs:181

Description: request bodies are capped at 10 MB, but upstream proxy responses are fully buffered with response.bytes(), and attested JSON responses are fully collected before quoting. The oauth3 service itself adds no rate limiting layer around /proxy, /verify, /attestation, or ?attest=true.

Impact: authenticated callers can request large upstream payloads or repeatedly force quote generation/verification to drive memory and CPU exhaustion.

Recommendation: stream proxy responses where possible, add response-size caps, and apply rate limiting on expensive oauth3 endpoints server-side rather than only in the Next.js frontend.

Informational Findings

FIND-012: On-chain attestation trust is anchored to a pinned key, not full Intel collateral or measurement verification

Severity: Informational

Location: contract.rs:88, contract.rs:132, verify.rs:199

Description: the clearance contract verifies the quote’s signature against a stored attestation key and checks REPORTDATA, but it does not validate Intel collateral, TCB state, or RTMR/event-log measurements on chain. Separately, /attestation-key simply extracts a key from a fresh quote without verifying that quote first. This is a trust-model caveat, not an immediate code execution bug.

Impact: the system’s security ultimately depends on trusted setup and off-chain verification when registering tee_pubkey. The current docs should not imply that full hardware/measurement verification happens independently on chain.

Recommendation: document the trusted setup explicitly, verify the quote and measurements off-chain before any set_tee_pubkey, and treat /attestation-key as an extraction helper, not a verifier.

Cross-Repo Integration Analysis

The integration boundary is the weakest part of the system. oauth3 exports a bearer token in a URL query string, and Redacted imports any token= callback into an authenticated cookie without correlating it to a locally initiated login. That turns the popup/callback bridge into a session-substitution point.

The contract only checks that result.address matches the transaction sender. It does not know which Google identity produced that result. That design is fine only if the browser-to-oauth3 session handoff is trustworthy. In the current code, it is not. Any token substitution at the Redacted callback layer becomes proof substitution at the contract layer.

One part of the boundary is implemented correctly: the popup postMessage uses a same-origin target, and VerifyFlow checks event.origin === window.location.origin. The bug is not postMessage; it is the unauthenticated GET callback that can set the cookie without any prior state.

Positive Observations

  • The OIDC login flow verifies state, uses PKCE for public clients, and verifies ID token claims with nonce handling via the openidconnect library. See oidc.rs:300 and oidc.rs:356.
  • OAuth authorization codes are single-use and consumed transactionally in both repo backends. See sqlite.rs:569 and pg.rs:390.
  • Consent revocation is re-checked during app-token authentication, so revoking consent blocks existing app access tokens. See session.rs:198.
  • I did not find a confirmed SQL injection path; the reviewed database access uses Diesel query builders rather than user-built SQL strings.
  • I did not find a confirmed CosmWasm reentrancy or arithmetic issue; the contract checks sender/address equality and rejects duplicate badge ownership before minting. See contract.rs:122 and contract.rs:149.
  • Redacted sets useful browser headers on its own origin (HSTS, nosniff, frame-ancestors 'self') and validates POST origins in middleware. See next.config.js:23 and middleware.ts:62.

Summary Table

ID Severity Title Location
FIND-001 Critical Gmail API failures are treated as “clean” results verify.rs:101
FIND-002 Critical Redacted callback blindly accepts arbitrary token= URLs callback route:6
FIND-003 High oauth3 exports bearer session tokens to arbitrary external return_to URLs by default oidc.rs:80
FIND-004 High Proxy forwards oauth3 cookies and internal headers to upstream providers proxy.rs:174
FIND-005 High /verify/gmail is callable with generic proxy API keys and app access tokens session.rs:93
FIND-006 High OAuth provider secrets and user provider tokens are stored plaintext in the database schema.rs:84
FIND-007 Medium `proxy:{provider}:read write` scopes are accepted but never enforced
FIND-008 Medium Redirect URI registration allows arbitrary plaintext http:// callbacks apps.rs:481
FIND-009 Medium Secure cookie protection depends on a manual APP_FORCE_SECURE flag session.rs:72
FIND-010 Medium next 14.2.29 is below published security fixes, and the app uses affected image features package.json:20
FIND-011 Low Proxy and attestation paths buffer entire responses in memory and are not server-throttled proxy.rs:231
FIND-012 Informational On-chain attestation trust is anchored to a pinned key, not full Intel collateral or measurement verification contract.rs:88

Natural next steps:

  1. Fix FIND-001 and FIND-002 before issuing any more badges.
  2. Replace the query-token callback handoff with a one-time code exchange and local state binding.
  3. Strip forwarded headers in the proxy and lock /verify/gmail to an explicit verification scope or browser session only.
  4. Encrypt provider secrets/tokens at rest and upgrade next to a patched release.

Consolidated Security Audit Report

Date: 2026-03-03 Scope: oauth3/ (Rust/Axum TEE OAuth server) + Redacted/ (Next.js + CosmWasm frontend) Method: Independent parallel audits by Claude Opus 4, Gemini 2.5 Pro, and OpenAI Codex (GPT-5.3), then consolidated with consensus scoring.


Methodology

Three AI-assisted static analysis tools audited the same codebase using an identical prompt. Each tool worked in isolation, reading source but unable to see other tools' output. Findings were then deduplicated and scored by consensus (3/3, 2/3, or 1/3 agreement). Severity conflicts are noted explicitly.

Legend: C = Claude Opus 4, G = Gemini 2.5 Pro, X = Codex GPT-5.3


Executive Summary

All three auditors converge on the same critical theme: the integration boundary between oauth3 and Redacted is the weakest point in the system. The popup-based OAuth callback flow lacks state binding, and the oauth3 server defaults to allowing arbitrary redirect targets with bearer tokens in the URL. A secondary critical finding — Gmail API errors being silently treated as "clean" — was flagged by two of three tools and would allow false clearance badges.

The TEE attestation chain is sound in its cryptographic design but architecturally incomplete: the smart contract verifies signatures and REPORTDATA binding, but does not verify MRTD/RTMR measurements, meaning any TEE on the same platform could produce valid-looking quotes.

Overall Risk: High. The top 3 fixes (open redirect default, callback state binding, Gmail fail-closed) should be addressed before any further badge issuance.

Finding counts by consensus:

  • 3/3 agreement: 3 findings
  • 2/3 agreement: 8 findings
  • 1/3 unique: 8 findings

Critical Findings

CON-001: Open Redirect Default Leaks Bearer Session Tokens

Consensus 3/3 (C: SEC-01 Critical, G: FIND-001 Critical, X: FIND-001 Critical)
Severity Critical
Location oauth3/src/auth/oidc.rs:111-117 (is_trusted_redirect)

Description: When ALLOWED_RETURN_TO_ORIGINS is unset, is_trusted_redirect() returns true for any URL. After login, redirect_after_login() appends a signed HMAC bearer session token to this URL. An attacker crafts return_to=https://evil.com/cb, receives the victim's valid token, and can use it against /verify/gmail and proxy endpoints.

Impact: Full session hijacking for any deployment following repo defaults. The stolen token enables impersonation across all bearer-token-protected endpoints.

Recommendation: Default to false (deny) when no allowlist is configured. Eliminate query-string bearer tokens entirely in favor of a one-time code exchange.


CON-002: Redacted Callback Accepts Arbitrary Tokens Without State Binding

Consensus 2/3 (X: FIND-003 High, X: FIND-002 Critical → merged; C does not flag separately but notes in cross-repo analysis)
Severity Critical (Codex), Implicit (Claude cross-repo analysis)
Location Redacted/src/app/api/oauth3/callback/route.ts:6-45

Description: The callback GET route stores any syntactically-valid token query param as the oauth3_token cookie. There is no Redacted-side state, nonce, or session binding. An attacker authenticates with their own clean Google account, captures their callback URL, and sends the victim to /api/oauth3/callback?token=<attacker-token>. The victim then mints a badge backed by the attacker's inbox state.

Impact: Complete integrity break of the clearance flow — a badge can be issued for wallet A using Gmail account B.

Recommendation: Generate a nonce in /api/oauth3/login, store in a short-lived cookie, require it in the callback. Exchange a one-time code server-to-server rather than accepting token= directly.


CON-003: Gmail API Errors Treated as "Clean" Results

Consensus 2/3 (X: FIND-001 Critical, X: FIND-002 Critical → mapped; G: not flagged)
Severity Critical
Location oauth3/src/web/handlers/verify.rs:36, 101-118

Description: GmailListResponse.result_size_estimate defaults to 0 via #[serde(default)]. The Gmail API request does not call .error_for_status(). A 403 insufficient-scope, quota failure, or structured error body can deserialize as result_size_estimate: 0, yielding clean: true. The shipped example config omits Gmail scopes entirely, making this the default behavior.

Impact: False-positive TDX-attested clearance badges. Users receive valid "clean" proofs without any inbox search actually succeeding.

Recommendation: Call .error_for_status() on the Gmail response before parsing. Reject bodies not matching the expected success schema. Validate that Google scopes include Gmail access at login time.


High Findings

CON-004: Placeholder OAuth Mode Creates Sessions Without Authentication

Consensus 1/3 (C: SEC-02 Critical)
Severity Critical (Claude)
Location oauth3/src/auth/oidc.rs:244-273

Description: When the OIDC callback receives no authorization code, state, or session cookie, it falls through to a placeholder path that creates a new user with a fabricated identity and issues a 60-minute authenticated session with zero verification.

Impact: Any unauthenticated request to the callback endpoint receives a valid session, bypassing the entire OAuth flow.

Recommendation: Remove the placeholder fallback or gate behind a compile-time dev-placeholder feature flag disabled in release builds.


CON-005: Missing MRTD/RTMR Verification in Smart Contract

Consensus 2/3 (G: FIND-002 Critical→High, X: FIND-012 Informational)
Severity High (Gemini), Informational (Codex) — resolved to High
Location Redacted/contracts/clearance/src/contract.rs:119-124

Description: The contract verifies the TDX quote signature against a stored attestation key and checks REPORTDATA hash binding, but does not verify MRTD (Measurement of the TD) or RTMR values. Any TEE on the same platform sharing the same attestation key could run a modified oauth3 binary that always returns clean: true.

Impact: A modified server in a same-platform TEE could produce valid-looking proofs, undermining the hardware root of trust.

Recommendation: Extract MRTD from the TDX report body and verify against a known-good measurement. Document the trusted setup explicitly.


CON-006: Proxy Forwards oauth3 Cookies to Upstream Providers

Consensus 1/3 (X: FIND-004 High)
Severity High
Location oauth3/src/web/proxy.rs:174-211

Description: forward_request() copies nearly all inbound headers (including the encrypted sid session cookie) to upstream providers. Only hop-by-hop headers are stripped.

Impact: Google/GitHub or any configured provider can observe oauth3 session material. A compromised provider could replay the cookie.

Recommendation: Use an explicit header allowlist. Strip cookie, set-cookie, authorization from outbound proxy requests.


CON-007: /verify/gmail Callable via Generic Proxy API Keys

Consensus 1/3 (X: FIND-005 High)
Severity High
Location oauth3/src/web/session.rs:93-176, oauth3/src/web/handlers/verify.rs:46

Description: SessionUser accepts API keys with proxy scope and OAuth app tokens on any route. /verify/gmail performs no route-specific authorization, so a third-party app token or API key can invoke Gmail verification.

Impact: Third-party apps can learn inbox contents and trigger side-effects (token revocation, identity unlinking) beyond their authorized scope.

Recommendation: Require a dedicated verify:gmail scope or restrict to interactive browser sessions.


CON-008: Provider Secrets and User Tokens Stored Plaintext

Consensus 1/3 (X: FIND-006 High)
Severity High
Location oauth3/src/schema.rs:84,115, oauth3/migrations/0001_init/up.sql:12

Description: oauth_providers.client_secret, user_identities.access_token, and user_identities.refresh_token are stored unencrypted. The database may be external to the TEE boundary.

Impact: A database compromise exposes Google/GitHub refresh tokens and provider client secrets, enabling long-lived API access outside the attested runtime.

Recommendation: Encrypt at rest using a key derived inside the TEE via DstackClient::derive_key().


CON-009: No CORS Configuration on oauth3 Server

Consensus 1/3 (C: SEC-05 High)
Severity High
Location oauth3/src/app.rs (no CorsLayer present)

Description: No explicit CORS middleware is applied. The server relies entirely on browser defaults.

Recommendation: Add an explicit CorsLayer with a strict origin allowlist.


CON-010: No Rate Limiting on oauth3 Authentication Endpoints

Consensus 2/3 (C: SEC-06 High, X: FIND-011 Low)
Severity High (Claude), Low (Codex) — resolved to High
Location oauth3/src/app.rs

Description: No rate limiting on any oauth3 endpoint. The Next.js frontend has memory-based rate limiting that resets on cold starts and is bypassable by calling oauth3 directly.

Recommendation: Add server-side rate limiting (e.g., tower-governor) on token, verification, and attestation endpoints.


CON-011: Token Expiry Check Fails Open on Parse Error

Consensus 2/3 (C: SEC-04 High, X: reference in FIND-003 description)
Severity High
Location oauth3/src/web/session.rs:250-258

Description: is_expired() returns false (not expired) when it can't parse the timestamp. A separate is_expired in proxy.rs correctly fails closed, making this inconsistency more concerning.

Recommendation: Change the fallback from false to true (treat unparseable = expired).


Medium Findings

CON-012: CSRF on OAuth Consent Page

Consensus 2/3 (G: FIND-003 Medium, C: mentioned but not flagged separately)
Severity Medium
Location oauth3/src/web/handlers/oauth.rs (consent form)

Description: The consent form lacks a CSRF token. Partially mitigated by SameSite=Lax cookies (blocks cross-site POST), but this is not a substitute for proper CSRF protection in all scenarios.


CON-013: return_to Cookie Not Encrypted

Consensus 1/3 (C: SEC-09 Medium)
Severity Medium
Location oauth3/src/auth/session.rs:52-59

Description: Uses cookies.add() instead of cookies.private().add(), making the redirect URL visible and modifiable by the client. Amplifies CON-001 if allowlist is misconfigured.


CON-014: Secure Cookie Flag Depends on Manual APP_FORCE_SECURE

Consensus 2/3 (X: FIND-009 Medium, G: FIND-008 Informational→Low)
Severity Medium
Location oauth3/src/auth/session.rs:35-72

Description: All session cookies are only marked Secure when APP_FORCE_SECURE is set. The example config defaults it to false. The code does not infer HTTPS from the public URL.

Recommendation: Default Secure=true when APP_PUBLIC_URL starts with https://.


CON-015: Proxy Scope read/write Suffixes Accepted but Never Enforced

Consensus 1/3 (X: FIND-007 Medium)
Severity Medium
Location oauth3/src/web/proxy.rs:37,156

Description: Scopes like proxy:google:read are validated at registration but proxy authorization only checks the provider prefix. A "read-only" token can perform writes.


CON-016: Redirect URI Registration Allows Plaintext http://

Consensus 1/3 (X: FIND-008 Medium)
Severity Medium
Location oauth3/src/web/handlers/apps.rs:239,481

Description: validate_redirect_uri() accepts any http or https URI with no fragment. Non-loopback http:// URIs expose authorization codes in transit.


CON-017: Replayable Attestation Quote (Static reportData)

Consensus 1/3 (G: FIND-004 Medium)
Severity Medium
Location oauth3/src/web/handlers/attestation.rs

Description: The /attestation endpoint always generates a quote with static reportData (app info JSON). Without a caller-supplied nonce, quotes are replayable.

Note: The /verify/gmail path uses request-specific reportData (result hash), so this only affects the standalone attestation endpoint.


CON-018: Client Secret Comparison Not Constant-Time

Consensus 2/3 (C: SEC-08 Medium, G: FIND-007 Low)
Severity Medium (practical risk low since comparison is on SHA-256 hashes)
Location oauth3/src/web/handlers/oauth.rs:515-516

CON-019: next 14.2.29 Below Published Security Fixes

Consensus 1/3 (X: FIND-010 Medium)
Severity Medium
Location Redacted/package.json:15

Description: Known advisories affect this Next.js version, and the app uses the affected next/image surface.


CON-020: CSP Uses unsafe-inline in Callback Route

Consensus 1/3 (C: SEC-11 Medium)
Severity Medium
Location Redacted/src/app/api/oauth3/callback/route.ts:39

Recommendation: Use nonce-based CSP.


CON-021: Session Cookies Bypass Proxy Scope Checks

Consensus 1/3 (C: SEC-07 Medium)
Severity Medium
Location oauth3/src/web/proxy.rs:156-159

Description: scopes: None (session auth) is treated as a wildcard granting access to all providers through the proxy.


Low Findings

CON-022: API Key Generation Uses Non-CSPRNG

Consensus 1/3 (C: SEC-03 High→Low adjusted)
Location oauth3/src/web/handlers/account.rs:224-238

Note: Claude rated this High; however, rand::thread_rng() on modern platforms uses ChaCha20 seeded from OS entropy and is cryptographically secure in practice. Downgraded to Low — still worth switching to OsRng for consistency.


CON-023: PII/Secret Leakage in Proxy Logs

Consensus 1/3 (G: FIND-005 Medium→Low adjusted)
Location oauth3/src/web/proxy.rs

Description: The proxy logs the full target_url including query strings, which may contain sensitive data.


CON-024: Hardcoded Dev Secrets in dex-config.yaml

Consensus 1/3 (G: FIND-006 Low)
Location oauth3/dex-config.yaml

CON-025: Memory-Based Rate Limiting Resets on Cold Start

Consensus 1/3 (C: SEC-15 Low)
Location Redacted/src/middleware.ts

Informational Findings

  • CON-026: Hardcoded suspect email jeevacation@gmail.com in 3 locations (C: SEC-16)
  • CON-027: Production CVM URL committed to repo (C: SEC-17)
  • CON-028: Dev cookie key auto-generated if unconfigured (C: SEC-18)
  • CON-029: Alpha-version Abstraxion dependency with compile-time patching (C: SEC-19)
  • CON-030: Minimal global CSP — only frame-ancestors 'self' (C: SEC-14)
  • CON-031: Only first 32 bytes of REPORTDATA validated in contract (C: SEC-10)

Cross-Repo Integration Analysis (Consensus View)

All three auditors agree the popup → callback → verify → mint pipeline is the critical attack surface:

  1. Login redirect — oauth3 accepts arbitrary return_to URLs by default and appends bearer tokens (CON-001). All 3 tools flagged this.

  2. Callback token injection — Redacted stores any syntactically-valid token= parameter without state binding (CON-002). 2/3 tools flagged this explicitly; the third noted it in cross-repo analysis.

  3. Gmail fail-open — API errors yield false "clean" results (CON-003). 2/3 tools flagged this.

  4. Contract verification gap — The contract verifies the attestation key signature and REPORTDATA binding but not the MRTD/RTMR measurements that identify the specific binary in the TEE (CON-005). 2/3 tools flagged this.

The postMessage channel between the popup and the parent window is correctly origin-validated — all three tools confirmed this as a positive.


Positive Observations (Consensus)

These were confirmed by multiple auditors:

Practice Confirmed By
PKCE S256 enforcement for public clients C, G, X
Authorization codes single-use and hash-stored C, G, X
OIDC token validation (signature, issuer, audience, nonce, expiry) C, G
Google token revocation + identity unlinking after verification C, X
Encrypted session cookies (private cookie jar) C, G
TDX quote REPORTDATA hash binding + address binding + 24h freshness C, G, X
Diesel ORM throughout (no raw SQL injection surface) C, G, X
SSRF protection in proxy (traversal checks, host validation) C, X
HTML escaping in consent page C, G
Arithmetic overflow checks enabled in contract release profile C
postMessage origin validation in VerifyFlow C, G, X
Consent revocation blocks existing app tokens X

Summary Table

ID Severity Consensus Title Location
CON-001 Critical 3/3 Open redirect default leaks bearer tokens oidc.rs:111-117
CON-002 Critical 2/3 Callback accepts tokens without state binding callback/route.ts:6-45
CON-003 Critical 2/3 Gmail API errors treated as "clean" verify.rs:36,101-118
CON-004 Critical 1/3 Placeholder mode creates sessions without auth oidc.rs:244-273
CON-005 High 2/3 Missing MRTD/RTMR verification in contract contract.rs:119-124
CON-006 High 1/3 Proxy forwards cookies to upstream providers proxy.rs:174-211
CON-007 High 1/3 /verify/gmail callable with generic proxy keys session.rs:93-176
CON-008 High 1/3 Provider secrets stored plaintext in DB schema.rs:84,115
CON-009 High 1/3 No CORS configuration on oauth3 app.rs
CON-010 High 2/3 No rate limiting on oauth3 endpoints app.rs
CON-011 High 2/3 Token expiry fails open on parse error session.rs:250-258
CON-012 Medium 2/3 CSRF on OAuth consent page oauth.rs
CON-013 Medium 1/3 return_to cookie not encrypted session.rs:52-59
CON-014 Medium 2/3 Secure cookie depends on manual env var session.rs:35-72
CON-015 Medium 1/3 Proxy scope read/write never enforced proxy.rs:37,156
CON-016 Medium 1/3 Redirect URI allows plaintext http:// apps.rs:239,481
CON-017 Medium 1/3 Replayable attestation quote attestation.rs
CON-018 Medium 2/3 Client secret comparison not constant-time oauth.rs:515-516
CON-019 Medium 1/3 Next.js below published security fixes package.json:15
CON-020 Medium 1/3 CSP uses unsafe-inline in callback callback/route.ts:39
CON-021 Medium 1/3 Session auth bypasses proxy scope checks proxy.rs:156-159
CON-022 Low 1/3 API key uses non-CSPRNG account.rs:224-238
CON-023 Low 1/3 PII/secrets in proxy logs proxy.rs
CON-024 Low 1/3 Hardcoded dev secrets dex-config.yaml
CON-025 Low 1/3 Memory-based rate limiting resets on cold start middleware.ts

Totals: 4 Critical, 7 High, 10 Medium, 4 Low, 6 Informational


Priority Fix Order

  1. CON-001 — Set ALLOWED_RETURN_TO_ORIGINS in production immediately; change default to deny-all
  2. CON-003 — Add .error_for_status() to Gmail API call; validate scopes at login
  3. CON-002 — Add state/nonce binding to Redacted callback flow
  4. CON-004 — Remove or feature-gate the placeholder auth mode
  5. CON-011 — Flip is_expired() fallback from false to true
  6. CON-005 — Add MRTD verification to the smart contract or document trust assumptions
  7. CON-008 — Encrypt provider secrets at rest using TEE-derived keys

Generated from independent audits by Claude Opus 4, Gemini 2.5 Pro, and OpenAI Codex (GPT-5.3) on 2026-03-03.

I will begin by exploring the oauth3 codebase, focusing on authentication, session management, and the TEE attestation logic. I'll list the handlers to identify the core logic for OAuth, authentication, and attestation. I'll explore the oauth3/src/auth/ directory to examine the implementation of token management and OIDC logic. I'll examine oauth3/src/web/handlers/auth.rs to understand authentication, session creation, and how the postMessage API is used in the callback logic. auth.rs is a thin wrapper, so I'll examine oauth3/src/auth/oidc.rs for the core implementation logic. I've identified a potential open redirect and token leakage vulnerability in is_trusted_redirect due to insecure default behavior when ALLOWED_RETURN_TO_ORIGINS is missing. I also noted a Placeholder mode and verified that cookie security attributes are generally well-configured. Next, I'll examine oauth3/src/auth/session.rs to audit session token creation and verification. session.rs implementation appears robust, utilizing AEAD for the sid cookie and HMAC-SHA256 with constant-time verification for cross-domain tokens. I've noted a potential issue where the Secure flag's activation is environment-dependent, which I'll monitor for production implications. I'll now examine oauth3/src/web/handlers/oauth.rs to audit the OAuth2 authorization server logic and its integration with the Next.js application. I've analyzed the OAuth2 implementation, noting robust PKCE and redirect URI validation, along with secure token hashing. However, I've identified a potential CSRF vulnerability in the authorize_post handler due to the absence of explicit CSRF tokens in the consent form. I'm also monitoring for sensitive data logging. My next step is to audit oauth3/src/web/handlers/attestation.rs to evaluate the TEE attestation logic, a cornerstone of the platform's security. I've analyzed oauth3/src/web/handlers/attestation.rs, noting proper app info binding but identifying a potential DoS vector due to expensive quote generation on every request. I'll now examine oauth3/src/web/handlers/verify.rs to determine how the Next.js app verifies its session with the server, as it's a critical piece of the integration. I've examined oauth3/src/web/handlers/verify.rs, noting the sound practice of revoking Gmail access tokens and unlinking identities after use. I'm investigating the attestation_key handler's extraction of a 64-byte key from offset 0x2BC of the TDX quote, which I suspect corresponds to the report_data field, but I need to confirm the exact offset for the TDX quote version in use. I'll also check how SessionUser is extracted to verify the authentication logic. oauth3/src/web/middleware.rs doesn't define SessionUser, so I'll check oauth3/src/web/session.rs for its implementation. SessionUser handles authentication and scope checks thoroughly. I'm now transitioning to the Redacted repository to examine the frontend's interaction with oauth3. I'll start by auditing the login and callback routes in Redacted/src/app/api/oauth3/, then move on to Redacted/src/components/VerifyFlow.tsx. Redacted/src/app/api/oauth3/login/route.ts redirects to oauth3/auth/google with a hardcoded PROD_URL and a return_to parameter. I suspect /oauth3 in the Next.js app proxies to the Rust server, so I'll verify this in Redacted/src/app/oauth3/[...path]/route.ts. The proxy handles Set-Cookie and 302 redirects correctly. I'll now examine Redacted/src/app/api/oauth3/callback/route.ts. In Redacted/src/app/api/oauth3/callback/route.ts, I've identified a potential vulnerability where postMessage uses request.nextUrl.origin as the target origin. If an attacker manipulates the return_to parameter—facilitated by the oauth3 server's insecure default behavior—they might interfere with the authentication flow. I'll now examine the receiver logic in Redacted/src/components/VerifyFlow.tsx to determine how it handles these messages and if it's susceptible to origin-based attacks. Redacted/src/components/VerifyFlow.tsx correctly validates postMessage origins, ensuring messages only come from the same origin. However, handleVerifyAndSubmit passes the result and quote directly to the smart contract. This introduces a potential client-side bypass if the contract doesn't rigorously verify that the quote's report_data matches the hash of the result. I'll now audit the smart contract at Redacted/contracts/clearance/src/contract.rs to confirm if this verification is implemented. I've audited the smart contract's execute_submit_proof and identified a critical vulnerability: it verifies the signature but fails to check MRTD or RTMRs. This means any TEE on the same platform could spoof a "clean" result, as the contract only confirms the quote was signed by a trusted attestation key, not that it originated from the authorized oauth3 binary. I'm now going to examine Redacted/contracts/clearance/src/msg.rs to understand the VerificationResult structure and further evaluate this risk. VerificationResult and CanonicalResult are consistent. I'm now auditing Redacted/src/app/api/oauth3/verify/route.ts to examine the server-side verification request. Redacted/src/app/api/oauth3/verify/route.ts correctly manages the oauth3_token cookie, but I'm investigating potential intercept risks. I'll now examine oauth3/src/web/handlers/apps.rs and oauth3/src/models/application.rs to audit the application registration process and the Next.js app's role as a registered entity. I've examined oauth3/src/web/handlers/apps.rs and confirmed that application management and scope validation are standard. I'll now audit Redacted/src/app/api/reclaim/init/route.ts to evaluate the Reclaim verification method. Reclaim flow uses jeevacation@gmail.com and properly handles RECLAIM_APP_SECRET server-side. I'm now auditing the "Clearance" smart contract's state.rs and error.rs to understand its state management and error handling. state.rs stores tee_pubkey in Config. I'll now examine Redacted/src/lib/contracts.ts to audit the frontend's blockchain interaction. Frontend contract interaction is straightforward, supporting both TEE and zkTLS verification with separate contract sets. I've confirmed that the oauth3 callback's HTML generation is safe from XSS due to proper escaping and regex validation. I'm now auditing oauth3/src/models/ to ensure Diesel's protections aren't bypassed by any raw SQL, which could introduce SQL injection vulnerabilities. I'll examine oauth3/src/repos/ to audit the Diesel queries for potential vulnerabilities. I'll examine oauth3/src/repos/sqlite.rs to audit the data access layer for potential vulnerabilities. sqlite.rs correctly uses Diesel's query builder, mitigating SQL injection risks. I'll now audit the authentication server configuration in oauth3/src/config.rs.

Security Audit Report

Executive Summary

This security audit evaluated the oauth3 authorization server and the Redacted Next.js frontend, focusing on their integration and the TEE-based trust model. The architecture is sophisticated, leveraging Intel TDX for verifiable execution and CosmWasm smart contracts for on-chain enforcement.

The overall security posture is strong in its use of modern cryptographic primitives and TEE hardware. However, a critical vulnerability was identified in the smart contract's attestation verification logic: it fails to verify the application's unique identity (MRTD), allowing any TEE on the same platform to potentially spoof results. Additionally, the oauth3 server defaults to an insecure state regarding redirect validation, which could lead to session token leakage.

Key recommendations include implementing strict MRTD checks in the smart contract, enforcing mandatory redirect allowlists in the authorization server, and adding CSRF protection to the OAuth consent flow.

Critical Findings

FIND-010: Missing Application Integrity (MRTD) Verification in Smart Contract

  • Severity: Critical
  • Location: Redacted/contracts/clearance/src/contract.rs, line 119-124
  • Description: The smart contract verifies that a TDX quote was cryptographically signed by a trusted platform attestation key and that the REPORTDATA matches the result hash. However, it does not verify the MRTD (Measurement of the TD) or RTMR values. These values uniquely identify the specific binary running in the TEE.
  • Impact: An attacker could run their own modified version of the oauth3 server in a TEE on the same platform (sharing the same Attestation Key). Their malicious server could always return clean: true regardless of actual Gmail contents. The smart contract would accept this spoofed proof as valid.
  • Recommendation: Extract the MRTD from the TDX report (typically at offset 184 within the report body) and verify it matches a known-good measurement of the oauth3 binary.

High Findings

FIND-001: Open Redirect & Session Token Leakage

  • Severity: High
  • Location: oauth3/src/auth/oidc.rs, lines 74-108 and redirect_after_login
  • Description: If the ALLOWED_RETURN_TO_ORIGINS environment variable is not configured, the server defaults to trusting all external redirect URLs. When a URL is "trusted," the redirect_after_login function appends a sensitive, signed session token as a query parameter (?token=...).
  • Impact: An attacker can initiate an auth flow with a malicious return_to parameter pointing to their server. If the admin hasn't set the allowlist, the oauth3 server will leak the user's session token to the attacker's server via the redirect.
  • Recommendation: Change the default behavior of is_trusted_redirect to return false for external origins if no allowlist is configured. Force users to explicitly define allowed origins.

Medium Findings

FIND-003: Missing CSRF Protection on OAuth Consent Page

  • Severity: Medium
  • Location: oauth3/src/web/handlers/oauth.rs, render_consent_page and authorize_post
  • Description: The OAuth2 consent form does not include a CSRF token. The authorize_post handler relies solely on the session cookie for authentication.
  • Impact: An attacker could potentially perform a "Consent Spoofing" attack by tricking a logged-in user into visiting a page that auto-submits a POST request to /oauth/authorize with decision=approve, granting the attacker's application access to the user's account without their informed consent.
  • Recommendation: Implement a standard CSRF protection mechanism (e.g., using a state token or a dedicated CSRF cookie) and verify it in the authorize_post handler.

FIND-005: DoS Vector via Attestation Endpoint

  • Severity: Medium
  • Location: oauth3/src/web/handlers/attestation.rs, line 48
  • Description: The /attestation endpoint generates a fresh TDX quote on every request. Generating a TEE quote is a computationally expensive operation involving the hardware's Quoting Enclave.
  • Impact: An unauthenticated attacker could flood the /attestation endpoint with requests, exhausting the TEE's quoting resources and potentially causing a Denial of Service for legitimate verification flows.
  • Recommendation: Implement aggressive rate limiting on the /attestation endpoint. Consider caching the quote for a short period (e.g., 1 minute) if the underlying application info hasn't changed.

FIND-007: postMessage Target Origin from Request Origin

  • Severity: Medium
  • Location: Redacted/src/app/api/oauth3/callback/route.ts, line 27
  • Description: The callback route uses request.nextUrl.origin as the target origin for postMessage.
  • Impact: If FIND-001 is exploited and the callback is somehow rendered on an attacker-controlled origin, this could facilitate cross-origin communication with an attacker-controlled opener. While the frontend receiver (FIND-008) has its own check, defense-in-depth suggests the sender should also be strict.
  • Recommendation: Hardcode the expected parent origin or use a strict allowlist instead of reflecting the current request origin.

Low Findings

FIND-002: Secure Cookie Flag Environment Dependency

  • Severity: Low
  • Location: oauth3/src/auth/session.rs, line 91
  • Description: The Secure attribute for session cookies is only set if APP_FORCE_SECURE is explicitly set to 1. In many environments, this might be forgotten, leading to session cookies being transmitted over unencrypted connections.
  • Impact: Potential session hijacking if the application is accessed over HTTP.
  • Recommendation: Default to Secure: true and only allow false if an explicit DEVELOPMENT_MODE flag is set.

FIND-011: Hardcoded Suspect Email Constraint

  • Severity: Low
  • Location: oauth3/src/web/handlers/verify.rs, line 13
  • Description: The Gmail verification logic is hardcoded to check for a single email address: jeevacation@gmail.com.
  • Impact: While not a direct security vulnerability, it limits the system's flexibility and makes the "clearance" binary specific to a single use case.
  • Recommendation: Allow the suspect email to be passed as a parameter but include it in the CanonicalResult so the smart contract can verify which suspect was checked.

Cross-Repo Integration Analysis

The integration between oauth3 and Redacted is well-reasoned:

  1. Popup Flow: Uses a popup to isolate the TEE's session from the main app's origin, which is a good pattern.
  2. Reverse Proxy: The Next.js app acts as a reverse proxy to oauth3, which allows it to handle Set-Cookie headers on the primary domain while bypassing some CORS complexities.
  3. Defense in Depth: oauth3 revokes Google access tokens immediately after use, minimizing the window of risk if a TEE was ever compromised.
  4. Token Handling: The oauth3_token is stored as an httpOnly cookie on the frontend and only passed to the proxy during the /verify call.

The weakest link is the trust anchor. The frontend relies on the smart contract to verify the TEE, but as noted in FIND-010, the contract's verification is incomplete.

Positive Observations

  • PKCE Enforcement: oauth3 correctly enforces PKCE for public clients.
  • Cryptographic Best Practices: Uses constant-time HMAC verification for session tokens and hashes tokens before storing them in the database.
  • PostMessage Receiver Security: The frontend VerifyFlow.tsx correctly validates the origin of incoming postMessage events.
  • TEE Binding: Application info is correctly bound to the attestation quote report data.

Summary Table

ID Severity Title Location
FIND-010 Critical Missing Application Integrity (MRTD) Verification Redacted/contracts/clearance/src/contract.rs
FIND-001 High Open Redirect & Session Token Leakage oauth3/src/auth/oidc.rs
FIND-003 Medium Missing CSRF Protection on OAuth Consent Page oauth3/src/web/handlers/oauth.rs
FIND-005 Medium DoS Vector via Attestation Endpoint oauth3/src/web/handlers/attestation.rs
FIND-007 Medium postMessage Target Origin Reflection Redacted/src/app/api/oauth3/callback/route.ts
FIND-002 Low Insecure Cookie Default (Secure Flag) oauth3/src/auth/session.rs
FIND-011 Low Hardcoded Verification Parameter oauth3/src/web/handlers/verify.rs
{
"timestamp": "20260303-131544",
"repo_root": "/Users/justin/IdeaProjects/redacted_omnibus",
"git_shas": {
"oauth3": "8a034e2a070b533d830afd14281a1f218354ed8d",
"redacted": "a04df084064110ab8b0dd9b4aff7fcdae1d06281"
},
"tool_versions": {
"claude": "2.1.63 (Claude Code)",
"gemini": "0.30.1",
"codex": "codex-cli 0.107.0"
},
"timeout_secs": 1800
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment