Skip to content

Fix/security scan findings - #24

Merged
sagargg merged 6 commits into
mainfrom
fix/security-scan-findings
Sep 24, 2026
Merged

sagargg merged 6 commits into
mainfrom
fix/security-scan-findings

Conversation

@sagargg

@sagargg sagargg commented Sep 24, 2026

Copy link
Copy Markdown
Member

No description provided.

Error envelopes returned the raw upstream exception, disclosing the
query engine's name, the cloud region and internal job identifiers to
anonymous callers.

`APIError` gains a `detail` field: `message` is what the caller sees,
`detail` is operator-only and reaches the log alone. Inverting the split
this way makes the safe path the default - a new `ServerError("...")`
without `detail` cannot leak. `str(exc)` still folds in the detail, so
tracebacks stay useful.

Every engine raise that interpolated an exception into `message` is
converted, plus two of the same shape found by scanning for the pattern:
the SQL-download dry run and the CKAN transport error, the latter
carrying the internal cluster address.

`_classify_bq_error` additionally maps BigQuery's caller-error reasons
(`invalidQuery`, `invalid`, `notFound`) to a 400 rather than a 500 - an
unknown column or a type-mismatched filter is the caller's mistake. The
reason is read from `errors[0]`, not the `reason` attribute, which is
None on a real `BadRequest`.

5xx now logs at ERROR with the detail and the original traceback; it was
DEBUG, so in production the detail reached the client and nothing else.
A `resource_id` containing a null byte was passed straight through to
CKAN, which 500'd on it, and the API surfaced that as `Internal Error`.
Malformed input is the caller's mistake and belongs in a 400 raised
before any upstream call.

Allow-list rather than block-list: `^[A-Za-z0-9_-]{1,200}$`. Blocking
the null byte would only invite blocking the newline next, then the
semicolon, and so on. The pattern covers both shapes actually in use -
CKAN UUIDs and the table-style names `datastore_create` accepts and the
docs' own examples use - so it rejects traversal, quotes, separators,
control characters and over-length values without narrowing the API. A
UUID-only rule, as the report suggested, would have broken documented
behaviour.

Applied to every `resource_id` field, to the `id` alias on info/delete
(validating only one of the pair would leave a bypass), and to the
`dump/{resource_id}` path parameter, which no request model covers.

The rejection message states what is allowed rather than echoing the
input, so control characters do not reach the response body or the logs.
A V4 signed URL carries the signer's service-account email in
`X-Goog-Credential`, where it is public. Signing with the rw account
advertised the write identity on every download redirect, and issued a
grant from an account that could also overwrite the object it points at.

A blob signs through whichever client listed it, and the export path
lists through rw, so `_signed_urls` re-binds each blob to the ro bucket
before signing. Both the cache-hit and cache-miss paths converge on this
one call, so neither can sign as rw.

Only signing moves. The export, compose, publish and GC steps still run
as rw, which they must.

Requires an IAM change before deploy: the ro service account needs
`iam.serviceAccountTokenCreator` on itself (IAM `signBlob` under
workload identity) plus `storage.objects.get`. Signing will fail at
runtime until that grant lands. Section 5.3's IAM table is updated to
match - it previously documented signing as an rw operation.

The service-account email cannot be removed from a signed URL; that is
inherent to the signing scheme. What changes is that the account it
names can only read.
Parquet exported `*`, keeping BigQuery's native TIMESTAMP, while
csv/ndjson cast to a whole-second ISO string. The same row therefore
read back as `2026-09-11T07:08:35` from a csv download and
`2026-09-11T07:08:35.908256` from a parquet one.

Parquet now applies the same `format_select_column` cast as the other
formats, keeping its JSON-to-string cast, which BigQuery still requires.
`*` is retained when a table has neither timestamp nor JSON columns, so
the common case keeps the short form.

Affects both download endpoints, which share this select-list builder.

Tradeoff: parquet now carries timestamps as strings rather than a native
TIMESTAMP column, which is what matching csv exactly costs - csv can
only carry text. If typed timestamps matter more than cross-format
equality, the alternative is widening the csv format string to
microseconds instead.
A request to a path with no route, or with the wrong method, came back
as FastAPI's default `{"detail": "Not Found"}` rather than the
`{help, success, error:{__type, message}}` envelope every other error
uses, so a client parsing the documented shape needed a special case.

`fastapi.HTTPException` subclasses Starlette's, and it is Starlette's
that the router raises for 404 and 405 - registering only the subclass
never caught them. Registering the base class alongside it covers both
without changing any existing behaviour.
`sort=", ,"` parsed to an empty list and was silently ignored, so a
client that built the string wrong got unsorted rows and a 200 rather
than being told. `"person_id,,first_name"` had the same problem mid-
string, silently dropping nothing but hiding the typo.

An empty term between commas is now a ValueError, which surfaces as a
400. A wholly blank `sort` still means "no sort" - that is absence
rather than a malformed value, and remains accepted.
@coderabbitai

coderabbitai Bot commented Sep 24, 2026

Copy link
Copy Markdown

Important

  • 🔍 Trigger review

This repository does not receive automatic reviews because it has fewer than 10 stars.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Advanced

Run ID: 05ff103a-d5a0-4c6d-891b-add29ec450d9


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@sagargg
sagargg merged commit 5f64561 into main Sep 24, 2026
1 of 2 checks passed
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