Skip to content

security: mandatory auth for the in-process Studio console + session cookie - #168

Open
russimicro wants to merge 1 commit into
FiveTechSoft:mainfrom
russimicro:pr/studio-embedded-auth-mandatory
Open

security: mandatory auth for the in-process Studio console + session cookie#168
russimicro wants to merge 1 commit into
FiveTechSoft:mainfrom
russimicro:pr/studio-embedded-auth-mandatory

Conversation

@russimicro

Copy link
Copy Markdown
Collaborator

OpenADS Studio — mandatory auth for the in-process (LocalServer) console, plus a session cookie that doesn't break the SPA's own fetch()

Repo FiveTechSoft/OpenADS
Reported / fixed by Russoft / Zerus ERP (Harbour 3.2 + rddads + OpenADS x64)
Date 2026-08-10
Files src/abi/studio_embed.cpp, tools/serverd/http_server.cpp
Severity High for problem 1 (an unauthenticated console is reachable by any process that loads the DLL), medium for problem 2 (breaks the console's own UI for any embedding host that tries the obvious URL-userinfo approach)

Context

We embed the Studio console in-process inside a small FiveWin/WebView2 shell (AdsStudioStart(port, dataDir), the LocalServer entry point — no openads_serverd involved). Building a security layer around that shell surfaced two issues in the shared code, independent of our own host.

Problem 1 — AdsStudioStart() bound an open listener by default

HttpConsole::add_user() already existed (studio.web.0.8, wired into openads_serverd via its config.ini), but the in-process entry point only called it if OPENADS_STUDIO_USER/OPENADS_STUDIO_PASSWORD happened to be set — bind first, auth optional.

Concretely: any other process on the same machine that loads ace64.dll/openace64.dll and calls AdsStudioStart() without setting those two env vars gets a fully open console — read/edit/delete on every table under the given data dir, no credentials asked, no warning. A host's own access-control layer (a license check, a login screen) can't protect an exported DLL function that any caller can invoke directly — ctypes in Python, P/Invoke in C#/PowerShell, or a ten-line .c file linking the import lib.

Fix

AdsStudioStart() now returns AE_LOGIN_FAILED (7077) instead of binding when the env vars are absent. try_auto_start() (the OPENADS_STUDIO_PORT env-var hook fired from DllMain) got the identical rule, so the auto-start convenience path can't create an unauthenticated listener either.

This is a behavior change: a caller that previously relied on the no-auth default for the in-process console needs to start setting OPENADS_STUDIO_USER/OPENADS_STUDIO_PASSWORD. We checked — nothing in this repo (source, tests, examples, docs samples) calls AdsStudioStart expecting the old default, but flagging this explicitly since it's the kind of change that deserves a maintainer's eyes before merging, not just a green CI run.

Problem 2 — Basic-auth via URL userinfo breaks the SPA's own fetch()

The obvious way to hand a generated per-session credential to an embedding webview host is http://user:pass@host:port/.... It works for the first navigation (the browser sends Authorization: Basic for that request), but once document.location carries userinfo, the Fetch spec refuses to construct a Request from any URL resolved against it:

Failed to execute 'fetch' on 'Window': Request cannot be constructed
from a URL that includes credentials: /api/tables

So the console's own fetch("/api/...") calls (used everywhere in the SPA — table list, browse, structure, SQL) throw immediately after the page loads. Confirmed against WebView2/Chromium; this isn't an OpenADS bug per se, it's a spec restriction that any embedding host hits the moment it tries the "obvious" URL-credential approach.

Fix

A one-time login path: GET /?_auth_user=U&_auth_pass=P (query string, not userinfo — a top-level navigation, not a fetch(), so it's unaffected by the restriction above). On a credential match the server:

  1. Sets an HttpOnly, SameSite=Strict session cookie (oads_auth, same base64 user:pass blob Basic-auth already used internally — user_pass_valid() verifies both the header and the cookie with one code path).
  2. 302-redirects to the same view (table/tab/host params preserved) without the credentials in the URL, so they never end up in document.location for the SPA's own JS to trip over.

From there every request — page loads and fetch() alike — authenticates via the cookie automatically (same-origin, no JS changes needed anywhere in the SPA). Wrong credentials get a 403, not a hang or a silent pass-through.

Audit trail

Every failed login attempt and every request rejected by the auth gate now goes through openads::mgmt::ErrorLog::instance().log(...) — the same ads_err.dbf the rest of the engine already writes to (readable via sp_mgGetErrorLog), tagged STUDIO_AUTH, with the client IP, method and path. A rejected attempt is never silent.

Verification

  • Full unit suite: 1324/1324 passed, 16 skipped (pre-existing, missing fixtures — unrelated to this change) on this branch (origin/main + these two files only).
  • Confirmed neither changed file is linked into openads_unit_tests (src/CMakeLists.txt only adds abi/studio_embed.cpp and tools/serverd/http_server.cpp to the openads_ace/openads_serverd targets, not openads_core) — mechanically, this change cannot regress anything the existing suite already exercises.
  • Manually exercised end-to-end against a FiveWin/WebView2 host: AdsStudioStart without the env vars set returns AE_LOGIN_FAILED and binds nothing; with them set, the query-param login → cookie → SPA loads and its fetch() calls succeed; wrong credentials on the login endpoint get 403; an unauthenticated direct request to any /api/* route gets 401 and lands in ads_err.dbf.

Scope note

This PR intentionally does not touch anything about openads_serverd's own config.ini-based auth (auth_user/http_user) — that path never goes through AdsStudioStart, calls HttpConsole::add_user() directly from its own main.cpp, and stays exactly as it was (auth optional, operator's decision). This PR is scoped to the in-process/LocalServer entry point only.

…cookie

**Problem 1 — AdsStudioStart() bound an open listener by default.**
HttpConsole::add_user() already existed (studio.web.0.8, wired into
openads_serverd via config.ini), but the in-process/LocalServer entry
point (AdsStudioStart, used by any app embedding ace64.dll/openace64.dll
directly) only called add_user() *if* OPENADS_STUDIO_USER/PASSWORD
happened to be set — bind-first, auth-optional. Any other process on the
box that loads the DLL and calls AdsStudioStart() without setting those
two env vars got a fully open console: read/edit/delete on every table
in the given data dir, no credentials asked. A host's own gate (a
license check, say) can't protect an export any caller can invoke
directly (ctypes / P-Invoke / a 10-line .c) — the DLL itself has to
refuse. AdsStudioStart() now returns AE_LOGIN_FAILED (7077) instead of
binding when the env vars are absent; try_auto_start() (the
OPENADS_STUDIO_PORT env-var hook fired from DllMain) got the same rule,
so the auto-start convenience path can't create an unauthenticated
listener either. No caller in this repo relies on the old no-auth
default from AdsStudioStart specifically.

**Problem 2 — Basic-auth via URL userinfo breaks the SPA's own fetch().**
The natural way to hand a generated-per-session credential to a webview
host is `http://user:pass@host:port/...`. It works for the initial
navigation, but once `document.location` carries userinfo, the Fetch
spec refuses to construct a Request from any URL resolved against it
("Request cannot be constructed from a URL that includes credentials"),
so the console's own `fetch("/api/...")` calls throw immediately after
load. Fix: a one-time login path, `GET /?_auth_user=U&_auth_pass=P`
(query string, not userinfo — unaffected by the restriction above,
since it's a top-level navigation, not a fetch()). On a match the
server sets an HttpOnly `oads_auth` session cookie (SameSite=Strict)
and 302-redirects to the same view without the credentials in the URL;
from there every request (page loads and fetch alike) authenticates via
the cookie automatically, same-origin, no JS changes required. Wrong
credentials get a 403, not a hang or a silent no-op.

**Audit trail.** Every failed login and every request rejected by the
auth gate now goes through openads::mgmt::ErrorLog (the same
ads_err.dbf the rest of the engine already writes to, readable via
sp_mgGetErrorLog) with the client IP, method and path — a rejected
attempt is never silent.

Verified: full unit suite green (1324/1324, 16 skipped — unrelated
missing fixtures) on this branch. Manually exercised end-to-end against
a FiveWin/WebView2 host (query-param login -> cookie -> SPA loads and
its fetch() calls succeed; AdsStudioStart without the env vars set
returns AE_LOGIN_FAILED and binds nothing).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant