Skip to content

fix: security hardening#5

Merged
skyoo2003 merged 10 commits intomainfrom
fix/security-hardening
Apr 19, 2026
Merged

fix: security hardening#5
skyoo2003 merged 10 commits intomainfrom
fix/security-hardening

Conversation

@skyoo2003
Copy link
Copy Markdown
Owner

Summary

  • XSS prevention: Sanitize Content-Type in API gateway to never serve text/html, preventing browser script execution on API responses
  • SQL injection defense: Add identifier validation in NewResourceStore to reject non-identifier characters in table/column names
  • Container security: Run as non-root appuser in both Dockerfile and Dockerfile.dev
  • Actions pinned: Pin all third-party GitHub Actions to commit SHAs (checkout, setup-go, docker/*, golangci-lint, release-drafter, create-pull-request)
  • Dependency update: Bump Next.js 16.2.3 → 16.2.4 (DoS fix)

Test plan

  • go build ./... passes
  • go test ./internal/gateway/... ./internal/shared/... passes
  • npm audit reports 0 vulnerabilities in web/
  • Pre-commit hooks pass

… actions

- Prevent XSS by sanitizing Content-Type in API gateway responses
- Add SQL identifier validation in shared ResourceStore constructor
- Run containers as non-root user in Dockerfile and Dockerfile.dev
- Pin all GitHub Actions to commit SHAs in cd, lint, release-drafter, smithy-sync
- Bump Next.js from 16.2.3 to 16.2.4
@sourcery-ai
Copy link
Copy Markdown
Contributor

sourcery-ai Bot commented Apr 19, 2026

Reviewer's Guide

This PR hardens security across the service by validating SQL identifiers in the shared store, sanitizing API gateway Content-Type headers to avoid serving HTML, running containers as a non-root user with a guarded entrypoint, pinning GitHub Actions to commit SHAs, and updating Next.js to a patched version.

File-Level Changes

Change Details Files
Add identifier validation to the shared ResourceStore constructor to prevent SQL injection via table and column names.
  • Change ResourceStore constructor to return (*ResourceStore, error) and trim/validate table, id column, and column list identifiers.
  • Introduce a validateIdentifier helper that enforces non-empty identifiers composed only of letters, digits, and underscores.
  • Update existing ResourceStore tests to handle constructor errors and add coverage for invalid and valid identifier cases, including whitespace and comma edge cases.
internal/shared/store.go
internal/shared/store_test.go
Sanitize response Content-Type in the API gateway to block text/html and enforce safe defaults.
  • Derive Content-Type from the response or existing headers with a fallback to application/octet-stream when unset.
  • Normalize and parse potentially comma-separated Content-Type values and detect text/html via mime.ParseMediaType with case-insensitive comparison.
  • Replace any HTML-like content types with text/plain; charset=utf-8 before writing headers and body.
internal/gateway/router.go
Run the application container as a non-root appuser with a hardened entrypoint.
  • Install su-exec and create a non-root appuser in the runtime image, setting /app as the working directory and adjusting ownership.
  • Add an entrypoint script that ensures /app/data exists and is owned by appuser before dropping privileges to appuser via su-exec.
  • Wire the new entrypoint into the runtime Dockerfile and ensure the command is executed as the non-root user.
  • Apply equivalent non-root adjustments in the development Dockerfile if present.
docker/Dockerfile
docker/Dockerfile.dev
docker/entrypoint.sh
Pin all third-party GitHub Actions to specific commit SHAs for supply-chain security.
  • Replace version tags for actions/checkout and actions/setup-go with explicit commit SHAs in CI workflows.
  • Pin Docker-related actions (metadata, setup-buildx, login, build-push) to specific SHAs in CD workflows.
  • Pin golangci-lint and peter-evans/create-pull-request, as well as release-drafter, to fixed commit SHAs across their respective workflows.
.github/workflows/cd.yml
.github/workflows/lint.yml
.github/workflows/smithy-sync.yml
.github/workflows/release-drafter.yml
Update the web frontend to a patched Next.js version with associated lockfile changes.
  • Bump Next.js dependency from 16.2.3 to 16.2.4 in the web package manifest.
  • Regenerate or update the package-lock file to reflect the new Next.js version and transitive dependency changes.
web/package.json
web/package-lock.json

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@github-actions github-actions Bot added dependencies Dependency updates ci CI/CD workflows and scripts docker Docker and container related web Web dashboard (Next.js) bug Something isn't working labels Apr 19, 2026
@skyoo2003 skyoo2003 self-assigned this Apr 19, 2026
Copy link
Copy Markdown
Contributor

@sourcery-ai sourcery-ai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've found 1 issue, and left some high level feedback:

  • The new validateIdentifier helper panics on invalid input and rejects anything beyond [a-zA-Z0-9_]; consider returning an error from NewResourceStore (or at least narrowing its visibility) so unexpected input doesn’t crash the process and to allow for more flexible identifier formats if needed (e.g., . for schemas).
  • In ServiceRouter.ServeHTTP, it might be safer to strings.TrimSpace the computed Content-Type before checking strings.HasPrefix(strings.ToLower(ct), "text/html") to avoid bypasses via leading whitespace.
  • The Next.js dependency was changed from an exact version to a caret range (16.2.3^16.2.4), which will allow future minor/patch upgrades; if the intent is a tightly controlled security bump, consider keeping an exact version pin instead.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- The new `validateIdentifier` helper panics on invalid input and rejects anything beyond `[a-zA-Z0-9_]`; consider returning an error from `NewResourceStore` (or at least narrowing its visibility) so unexpected input doesn’t crash the process and to allow for more flexible identifier formats if needed (e.g., `.` for schemas).
- In `ServiceRouter.ServeHTTP`, it might be safer to `strings.TrimSpace` the computed `Content-Type` before checking `strings.HasPrefix(strings.ToLower(ct), "text/html")` to avoid bypasses via leading whitespace.
- The Next.js dependency was changed from an exact version to a caret range (`16.2.3``^16.2.4`), which will allow future minor/patch upgrades; if the intent is a tightly controlled security bump, consider keeping an exact version pin instead.

## Individual Comments

### Comment 1
<location path="web/package.json" line_range="17" />
<code_context>
         "clsx": "^2.1.1",
         "lucide-react": "^1.7.0",
-        "next": "16.2.3",
+        "next": "^16.2.4",
         "react": "19.2.4",
         "react-dom": "19.2.4",
</code_context>
<issue_to_address>
**suggestion (bug_risk):** Using a caret range for Next may introduce unanticipated upgrades

Switching from a pinned `16.2.3` to `^16.2.4` means any `16.x` release can be installed, including via local installs or partial lockfile updates, which can introduce unexpected behavior in a core framework like Next. If you prefer stability over automatic minor/patch updates, consider keeping Next pinned and updating it manually instead.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment thread web/package.json Outdated
- Change validateIdentifier from panic to returning error in NewResourceStore
- Add TrimSpace to Content-Type before HTML check to prevent whitespace bypass
- Revert Next.js to exact version pin (16.2.4) instead of caret range
@github-actions github-actions Bot added the tests Test code and test infrastructure label Apr 19, 2026
@skyoo2003
Copy link
Copy Markdown
Owner Author

@sourcery-ai review

Copy link
Copy Markdown
Contributor

@sourcery-ai sourcery-ai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've found 2 issues, and left some high level feedback:

  • In ServiceRouter.ServeHTTP, consider using mime.ParseMediaType to parse and compare the media type instead of strings.HasPrefix on the raw header value so that text/html detection is robust to parameters and odd casing/spacing without accidentally matching types like text/htmlx.
  • The validateIdentifier logic will fail if cols contains a trailing comma or patterns like * that some SQL helpers use; it may be worth explicitly documenting/guarding these cases or normalizing cols (e.g., filtering out empty segments) to avoid unexpected runtime errors for existing callers.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- In `ServiceRouter.ServeHTTP`, consider using `mime.ParseMediaType` to parse and compare the media type instead of `strings.HasPrefix` on the raw header value so that `text/html` detection is robust to parameters and odd casing/spacing without accidentally matching types like `text/htmlx`.
- The `validateIdentifier` logic will fail if `cols` contains a trailing comma or patterns like `*` that some SQL helpers use; it may be worth explicitly documenting/guarding these cases or normalizing `cols` (e.g., filtering out empty segments) to avoid unexpected runtime errors for existing callers.

## Individual Comments

### Comment 1
<location path="docker/Dockerfile" line_range="24-27" />
<code_context>
 # Stage 3: Runtime
 FROM alpine:3.20
 RUN apk add --no-cache sqlite-libs ca-certificates
+RUN adduser -D -H -h /app appuser
 WORKDIR /app
 COPY --from=go-builder /devcloud /app/devcloud
</code_context>
<issue_to_address>
**issue:** Non-root user may lack write permissions for `/app/data` at runtime.

Because the container now runs as `appuser`, only files owned at build time are guaranteed to be writable. Volumes declared with `VOLUME /app/data` are created at runtime and usually owned by root, so `appuser` may not be able to write there. If the app writes to `/app/data`, either handle `chown`/`chmod` for that path in an entrypoint/init step, or document that the volume must be created with appropriate ownership/permissions when running the container.
</issue_to_address>

### Comment 2
<location path="internal/shared/store_test.go" line_range="41-43" />
<code_context>
 	require.NoError(t, err)
 	t.Cleanup(func() { _ = db.Close() })
-	return NewResourceStore[testItem](db, "items", "id", "id, name", testScanner)
+	rs, err := NewResourceStore[testItem](db, "items", "id", "id, name", testScanner)
+	require.NoError(t, err)
+	return rs
 }

</code_context>
<issue_to_address>
**suggestion (testing):** Consider adding negative tests for `NewResourceStore` alongside this positive path.

To better cover the hardening, please also add tests that call `NewResourceStore` with invalid identifiers and assert that an error is returned (and, if practical, that the error message is as expected). Keeping these alongside the happy-path tests will ensure the constructor’s failure modes stay well covered.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment thread docker/Dockerfile
Comment thread internal/shared/store_test.go
- Use mime.ParseMediaType for robust text/html detection in gateway
- Add entrypoint.sh to handle /app/data volume permissions at runtime
- Skip empty segments in validateIdentifier to tolerate trailing commas
- Add negative tests for NewResourceStore with invalid identifiers
@skyoo2003
Copy link
Copy Markdown
Owner Author

@sourcery-ai review

Copy link
Copy Markdown
Contributor

@sourcery-ai sourcery-ai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've found 3 issues, and left some high level feedback:

  • In the runtime Dockerfile, the COPY lines for devcloud, codegen, web/out, devcloud.yaml, smithy-models, and templates are duplicated; consider removing the duplicates to avoid confusion and unnecessary rebuild work.
  • The container now runs as appuser, but docker/entrypoint.sh calls chown appuser:appuser /app/data, which will fail when executed as a non-root user; you may want to either perform the chown in the Dockerfile as root or drop the chown from the entrypoint.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- In the runtime Dockerfile, the `COPY` lines for `devcloud`, `codegen`, `web/out`, `devcloud.yaml`, `smithy-models`, and `templates` are duplicated; consider removing the duplicates to avoid confusion and unnecessary rebuild work.
- The container now runs as `appuser`, but `docker/entrypoint.sh` calls `chown appuser:appuser /app/data`, which will fail when executed as a non-root user; you may want to either perform the `chown` in the Dockerfile as root or drop the `chown` from the entrypoint.

## Individual Comments

### Comment 1
<location path="docker/Dockerfile" line_range="26-27" />
<code_context>
 RUN apk add --no-cache sqlite-libs ca-certificates
+RUN adduser -D -H -h /app appuser
 WORKDIR /app
 COPY --from=go-builder /devcloud /app/devcloud
 COPY --from=go-builder /codegen /app/codegen
</code_context>
<issue_to_address>
**issue:** The COPY instructions for binaries, config, and assets are duplicated and can be consolidated.

The block copying `/devcloud`, `/codegen`, `web/out`, `devcloud.yaml`, `smithy-models/`, and `templates/` is repeated in the final stage, which unnecessarily increases image size and complicates maintenance. Unless there is a deliberate layering requirement, you can remove the duplicate COPY block and keep a single instance.
</issue_to_address>

### Comment 2
<location path="docker/entrypoint.sh" line_range="6" />
<code_context>
+
+# Ensure the data directory exists and is writable by appuser.
+mkdir -p /app/data
+chown appuser:appuser /app/data
+
+exec "$@"
</code_context>
<issue_to_address>
**issue (bug_risk):** Running `chown` as the unprivileged `appuser` will likely fail, especially with bind mounts.

Since the Dockerfile switches to `USER appuser`, this `chown` runs as a non-root user and will usually fail with `EPERM` (especially when `/app/data` is a root-owned bind mount). With `set -e` enabled, that will terminate the container at startup. You could either move this `chown` into the Dockerfile before dropping privileges, run the entrypoint as root and then `su-exec`/`gosu` to `appuser`, or drop the `chown` and rely on the existing `/app` ownership if that’s sufficient.
</issue_to_address>

### Comment 3
<location path="internal/shared/store_test.go" line_range="149-153" />
<code_context>
+	assert.ErrorContains(t, err, "empty table identifier")
+}
+
+func TestNewResourceStore_TrailingComma(t *testing.T) {
+	db := newTestDB(t)
+	rs, err := NewResourceStore[testItem](db, "items", "id", "id, name,", testScanner)
+	require.NoError(t, err)
+	require.NotNil(t, rs)
+}
</code_context>
<issue_to_address>
**suggestion (testing):** Consider tests for valid identifiers (underscores/digits) to guard against over-restrictive changes

Current tests only cover rejection of invalid identifiers. Please also add a few positive cases (e.g. `items_v2`, `item2`, `user_profile_01`) to confirm underscores and digits remain accepted and to catch regressions if `validateIdentifier` is tightened in the future.

Suggested implementation:

```golang
func TestNewResourceStore_EmptyTable(t *testing.T) {
	db := newTestDB(t)
	_, err := NewResourceStore[testItem](db, "", "id", "id", testScanner)
	assert.ErrorContains(t, err, "empty table identifier")
}

func TestNewResourceStore_ValidIdentifiers(t *testing.T) {
	db := newTestDB(t)

	tests := []struct {
		name      string
		tableName string
		primary   string
		cols      string
	}{
		{
			name:      "table_with_version_suffix",
			tableName: "items_v2",
			primary:   "id1",
			cols:      "id1, name, created_at",
		},
		{
			name:      "table_with_digit_suffix",
			tableName: "item2",
			primary:   "id2",
			cols:      "id2, description, updated_at",
		},
		{
			name:      "table_with_underscores_and_digits",
			tableName: "user_profile_01",
			primary:   "user_id_01",
			cols:      "user_id_01, profile_name, last_login_2",
		},
	}

	for _, tc := range tests {
		t.Run(tc.name, func(t *testing.T) {
			rs, err := NewResourceStore[testItem](db, tc.tableName, tc.primary, tc.cols, testScanner)
			require.NoError(t, err)
			require.NotNil(t, rs)
		})
	}
}

func TestResourceStore_GetNotFound(t *testing.T) {
	require.NoError(t, err)
	assert.Equal(t, 1, n)
}

```

If `NewResourceStore` validates table/column identifiers against the actual database schema (rather than just using `validateIdentifier`), you may need to ensure the test migrations in `testMigrations` create tables/columns matching the names used in the new test cases. In that case, either:
1) Add corresponding tables/columns (`items_v2`, `item2`, `user_profile_01`, etc.) to `testMigrations`, or  
2) Adjust the test case identifiers to match whatever tables/columns already exist in your test schema while still covering underscores and digits (e.g. `items_v2`, `items2`, `user_profile_01` using existing columns).
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment thread docker/Dockerfile
Comment thread docker/entrypoint.sh Outdated
Comment thread internal/shared/store_test.go
- Remove duplicate COPY instructions in Dockerfile
- Fix entrypoint to run as root with su-exec for privilege drop
- Add positive identifier validation tests (underscores, digits, trailing comma)
@skyoo2003
Copy link
Copy Markdown
Owner Author

@sourcery-ai review

Copy link
Copy Markdown
Contributor

@sourcery-ai sourcery-ai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've found 1 issue, and left some high level feedback:

  • The new validateIdentifier logic only permits ASCII letters/digits/underscores, which will reject otherwise valid SQL identifiers (e.g., schema-qualified names like schema.table, quoted names, or names with dashes); if those are possible now or in the future, consider broadening the allowed syntax or documenting the constraint at the call sites.
  • By unconditionally normalizing text/html responses to text/plain in the gateway, any API endpoints that intentionally return HTML (e.g., human-facing error pages or debug views) will now be forced to plain text; consider adding an explicit opt-out or path/hostname-based allowlist for trusted HTML responses if that use case exists.
  • In the Docker image, /app is chowned recursively at build time and /app/data is chowned again on container start; if the directory tree grows large or is a mounted volume this double chown could become expensive, so you might restrict ownership changes to just the data directory or rely solely on build-time ownership where possible.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- The new `validateIdentifier` logic only permits ASCII letters/digits/underscores, which will reject otherwise valid SQL identifiers (e.g., schema-qualified names like `schema.table`, quoted names, or names with dashes); if those are possible now or in the future, consider broadening the allowed syntax or documenting the constraint at the call sites.
- By unconditionally normalizing `text/html` responses to `text/plain` in the gateway, any API endpoints that intentionally return HTML (e.g., human-facing error pages or debug views) will now be forced to plain text; consider adding an explicit opt-out or path/hostname-based allowlist for trusted HTML responses if that use case exists.
- In the Docker image, `/app` is chowned recursively at build time and `/app/data` is chowned again on container start; if the directory tree grows large or is a mounted volume this double chown could become expensive, so you might restrict ownership changes to just the data directory or rely solely on build-time ownership where possible.

## Individual Comments

### Comment 1
<location path="internal/gateway/router.go" line_range="56-58" />
<code_context>
+	if ct == "" {
+		ct = "application/octet-stream"
+	}
+	// Prevent XSS: never serve API responses as text/html.
+	ct = strings.TrimSpace(ct)
+	if mediaType, _, parseErr := mime.ParseMediaType(ct); parseErr == nil && strings.EqualFold(mediaType, "text/html") {
+		ct = "text/plain; charset=utf-8"
+	}
</code_context>
<issue_to_address>
**🚨 suggestion (security):** Content-Type XSS protection can be bypassed if upstream sets a comma-separated list of media types.

This guard only works when `mime.ParseMediaType(ct)` succeeds. If an upstream sets something like `"text/html; charset=utf-8, text/plain"`, parsing will fail and the fallback won’t apply, while a browser may still treat the response as HTML based on the first type. To harden this, consider splitting on `","` first and running the `ParseMediaType` / `text/html` check on the first (or all) entries, then normalizing to a safe value if any resolve to HTML-like types.

Suggested implementation:

```golang
	}
	ct := resp.ContentType
	if ct == "" {
		ct = w.Header().Get("Content-Type")
	}
	if ct == "" {
		ct = "application/octet-stream"
	}
	// Prevent XSS: never serve API responses as text/html.
	// Harden against comma-separated lists of media types by inspecting each entry.
	ct = strings.TrimSpace(ct)
	if ct != "" {
		parts := strings.Split(ct, ",")
		htmlLike := false
		for _, p := range parts {
			p = strings.TrimSpace(p)
			if p == "" {
				continue
			}
			if mediaType, _, err := mime.ParseMediaType(p); err == nil && strings.EqualFold(mediaType, "text/html") {
				htmlLike = true
				break
			}
		}
		if htmlLike {
			// Normalize any HTML-like content types to a safe, non-HTML type.
			ct = "text/plain; charset=utf-8"
		} else {
			// Normalize Content-Type to the first media type.
			ct = strings.TrimSpace(parts[0])
		}
	}

```

To compile, you also need to:

1. Ensure `internal/gateway/router.go` imports the packages:

   ```go
   import (
       "mime"
       "strings"
       // existing imports...
   )
   ```

   If there is already an `import (` block, add `"mime"` and `"strings"` inside it without duplicating existing imports.

2. If this function later uses `ct` to set the `Content-Type` header or to write the response, leave that logic unchanged so it benefits from the stricter normalization.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment thread internal/gateway/router.go Outdated
Split on commas and inspect each entry individually so that values like
"text/html, text/plain" cannot bypass the HTML sanitization.
@skyoo2003
Copy link
Copy Markdown
Owner Author

@sourcery-ai review

Copy link
Copy Markdown
Contributor

@sourcery-ai sourcery-ai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've left some high level feedback:

  • In NewResourceStore/validateIdentifier, table and idCol are validated without trimming whitespace (unlike cols), so a config value with leading/trailing spaces will be rejected rather than normalized; consider strings.TrimSpace on those inputs before validation to align behavior and avoid surprising failures.
  • The docker/entrypoint.sh unconditionally chowns /app/data on every container start, which can be slow on large or mounted volumes; you might gate this on current ownership (e.g., if [ "$(stat -c %U /app/data)" != "appuser" ]; then chown ...; fi) or adjust the volume/ Dockerfile so it’s only needed once.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- In `NewResourceStore`/`validateIdentifier`, `table` and `idCol` are validated without trimming whitespace (unlike `cols`), so a config value with leading/trailing spaces will be rejected rather than normalized; consider `strings.TrimSpace` on those inputs before validation to align behavior and avoid surprising failures.
- The `docker/entrypoint.sh` unconditionally `chown`s `/app/data` on every container start, which can be slow on large or mounted volumes; you might gate this on current ownership (e.g., `if [ "$(stat -c %U /app/data)" != "appuser" ]; then chown ...; fi`) or adjust the volume/ Dockerfile so it’s only needed once.

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

…t chown

- TrimSpace table and idCol in NewResourceStore to match cols behavior
- Only chown /app/data if not already owned by appuser
@skyoo2003
Copy link
Copy Markdown
Owner Author

@sourcery-ai review

Copy link
Copy Markdown
Contributor

@sourcery-ai sourcery-ai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've found 2 issues

Prompt for AI Agents
Please address the comments from this code review:

## Individual Comments

### Comment 1
<location path="docker/entrypoint.sh" line_range="6" />
<code_context>
+
+# Ensure the data directory exists and is writable by appuser.
+mkdir -p /app/data
+if [ "$(stat -c %U /app/data 2>/dev/null)" != "appuser" ]; then
+    chown appuser:appuser /app/data
+fi
</code_context>
<issue_to_address>
**issue (bug_risk):** Using `set -e` with `stat` in command substitution can cause unexpected exits.

With `set -e` enabled, a non‑zero exit from `stat` (e.g., due to environment differences or a different `stat` implementation) will terminate the script before the `if` check, even with stderr redirected. To make this fail open instead of aborting, consider temporarily disabling `set -e` around `stat`, or use something like:

```sh
owner=$(stat -c %U /app/data 2>/dev/null || echo "")
if [ "$owner" != "appuser" ]; then
    chown appuser:appuser /app/data
fi
```
</issue_to_address>

### Comment 2
<location path="internal/shared/store_test.go" line_range="159-183" />
<code_context>
+	require.NotNil(t, rs)
+}
+
+func TestNewResourceStore_ValidIdentifiers(t *testing.T) {
+	db := newTestDB(t)
+
+	tests := []struct {
+		name      string
+		tableName string
+		primary   string
+		cols      string
+	}{
+		{
+			name:      "underscores",
+			tableName: "items",
+			primary:   "id",
+			cols:      "id, item_name, created_at",
+		},
+		{
+			name:      "digits_in_identifiers",
+			tableName: "items",
+			primary:   "id",
+			cols:      "id, name2, col3_v1",
+		},
+		{
+			name:      "trailing_comma",
+			tableName: "items",
+			primary:   "id",
+			cols:      "id, name,",
+		},
+	}
+
+	for _, tc := range tests {
</code_context>
<issue_to_address>
**suggestion (testing):** Extend valid identifier table tests to cover whitespace and leading-comma cases

To better exercise the new normalization/parsing behavior, please add cases with surrounding whitespace in table/column identifiers (e.g. `" items "`, `" id "`, `" id, name "`) and a case with a leading comma in `cols` (e.g. `", id, name"`) to verify empty leading segments are ignored and valid segments are accepted.

```suggestion
	tests := []struct {
		name      string
		tableName string
		primary   string
		cols      string
	}{
		{
			name:      "underscores",
			tableName: "items",
			primary:   "id",
			cols:      "id, item_name, created_at",
		},
		{
			name:      "digits_in_identifiers",
			tableName: "items",
			primary:   "id",
			cols:      "id, name2, col3_v1",
		},
		{
			name:      "trailing_comma",
			tableName: "items",
			primary:   "id",
			cols:      "id, name,",
		},
		{
			name:      "whitespace_in_identifiers",
			tableName: " items ",
			primary:   " id ",
			cols:      " id, name ",
		},
		{
			name:      "leading_comma_in_cols",
			tableName: "items",
			primary:   "id",
			cols:      ", id, name",
		},
	}
```
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment thread docker/entrypoint.sh Outdated
Comment on lines +159 to +183
tests := []struct {
name string
tableName string
primary string
cols string
}{
{
name: "underscores",
tableName: "items",
primary: "id",
cols: "id, item_name, created_at",
},
{
name: "digits_in_identifiers",
tableName: "items",
primary: "id",
cols: "id, name2, col3_v1",
},
{
name: "trailing_comma",
tableName: "items",
primary: "id",
cols: "id, name,",
},
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion (testing): Extend valid identifier table tests to cover whitespace and leading-comma cases

To better exercise the new normalization/parsing behavior, please add cases with surrounding whitespace in table/column identifiers (e.g. " items ", " id ", " id, name ") and a case with a leading comma in cols (e.g. ", id, name") to verify empty leading segments are ignored and valid segments are accepted.

Suggested change
tests := []struct {
name string
tableName string
primary string
cols string
}{
{
name: "underscores",
tableName: "items",
primary: "id",
cols: "id, item_name, created_at",
},
{
name: "digits_in_identifiers",
tableName: "items",
primary: "id",
cols: "id, name2, col3_v1",
},
{
name: "trailing_comma",
tableName: "items",
primary: "id",
cols: "id, name,",
},
}
tests := []struct {
name string
tableName string
primary string
cols string
}{
{
name: "underscores",
tableName: "items",
primary: "id",
cols: "id, item_name, created_at",
},
{
name: "digits_in_identifiers",
tableName: "items",
primary: "id",
cols: "id, name2, col3_v1",
},
{
name: "trailing_comma",
tableName: "items",
primary: "id",
cols: "id, name,",
},
{
name: "whitespace_in_identifiers",
tableName: " items ",
primary: " id ",
cols: " id, name ",
},
{
name: "leading_comma_in_cols",
tableName: "items",
primary: "id",
cols: ", id, name",
},
}

…t cases

- Use || echo "" fallback for stat to prevent set -e from aborting
- Add test cases for whitespace-padded identifiers and leading commas in cols
@skyoo2003
Copy link
Copy Markdown
Owner Author

@sourcery-ai review

Copy link
Copy Markdown
Contributor

@sourcery-ai sourcery-ai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've found 1 issue, and left some high level feedback:

  • In NewResourceStore, you validate and trim individual column identifiers but keep the original cols string (including whitespace/leading commas); consider reconstructing a normalized, validated cols string from the parsed identifiers to avoid subtle SQL issues from caller-provided formatting.
  • Now that NewResourceStore returns an error, you might want to add a small helper (e.g., MustNewResourceStore) for call sites that should treat misconfiguration as fatal, to keep call sites concise while still centralizing the validation logic.
  • After pinning GitHub Actions to SHAs, it can be helpful to add inline comments with the corresponding version tags (e.g., # v6) so future updates are easier to manage without having to look up each commit.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- In `NewResourceStore`, you validate and trim individual column identifiers but keep the original `cols` string (including whitespace/leading commas); consider reconstructing a normalized, validated `cols` string from the parsed identifiers to avoid subtle SQL issues from caller-provided formatting.
- Now that `NewResourceStore` returns an error, you might want to add a small helper (e.g., `MustNewResourceStore`) for call sites that should treat misconfiguration as fatal, to keep call sites concise while still centralizing the validation logic.
- After pinning GitHub Actions to SHAs, it can be helpful to add inline comments with the corresponding version tags (e.g., `# v6`) so future updates are easier to manage without having to look up each commit.

## Individual Comments

### Comment 1
<location path="internal/gateway/router.go" line_range="56-70" />
<code_context>
+	// Prevent XSS: this gateway serves AWS API responses only (JSON/XML),
+	// never user-facing HTML. Sanitize any attempt to serve text/html.
+	ct = strings.TrimSpace(ct)
+	htmlLike := false
+	for _, p := range strings.Split(ct, ",") {
+		p = strings.TrimSpace(p)
+		if p == "" {
+			continue
+		}
+		if mediaType, _, parseErr := mime.ParseMediaType(p); parseErr == nil && strings.EqualFold(mediaType, "text/html") {
+			htmlLike = true
+			break
</code_context>
<issue_to_address>
**🚨 suggestion (security):** HTML sanitization only handles `text/html` and may miss other HTML-capable media types

Right now only `text/html` is treated as HTML-like, but browsers may also render HTML for types like `application/xhtml+xml` or custom `text/*+html`. If an attacker can influence the `Content-Type`, they could bypass this check while still getting HTML rendered. Consider extending the detection to cover `text/html`, `application/xhtml+xml`, and possibly `*+html` suffixes, while still disallowing user-facing HTML and only permitting the expected JSON/XML responses.

```suggestion
	// Prevent XSS: this gateway serves AWS API responses only (JSON/XML),
	// never user-facing HTML. Sanitize any attempt to serve HTML-like content types.
	ct = strings.TrimSpace(ct)
	htmlLike := false
	for _, p := range strings.Split(ct, ",") {
		p = strings.TrimSpace(p)
		if p == "" {
			continue
		}
		mediaType, _, parseErr := mime.ParseMediaType(p)
		if parseErr != nil {
			continue
		}
		mtLower := strings.ToLower(mediaType)
		if mtLower == "text/html" ||
			mtLower == "application/xhtml+xml" ||
			strings.HasSuffix(mtLower, "+html") {
			htmlLike = true
			break
		}
	}
	if htmlLike {
```
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment thread internal/gateway/router.go
- Reconstruct cols string from validated identifiers to strip whitespace/commas
- Extend Content-Type XSS guard to cover application/xhtml+xml and *+html suffixes
- Add inline version tag comments (e.g. # v6) to all pinned action SHAs
@skyoo2003 skyoo2003 merged commit 9d13e84 into main Apr 19, 2026
10 checks passed
@skyoo2003 skyoo2003 deleted the fix/security-hardening branch April 19, 2026 14:19
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working ci CI/CD workflows and scripts dependencies Dependency updates docker Docker and container related tests Test code and test infrastructure web Web dashboard (Next.js)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant