security: harden plugin sandbox stdin and opaque internal errors

- sandbox_wazero.go: explicitly discard plugin stdin via WithStdin to
  prevent WASM modules from reading the server process's stdin fd
- channel_handler.go: replace ErrInternal message pass-through with
  generic 'an internal error occurred' — full error stays server-side
  in slog.Error only
- invite_handler_test.go: update assertions to expect generic message
This commit is contained in:
J3vb
2026-04-07 07:59:10 +02:00
parent bd45b65dd3
commit 670e6e0323
3 changed files with 9 additions and 8 deletions
+1 -2
View File
@@ -286,8 +286,7 @@ func writeServiceError(w http.ResponseWriter, err error) {
writeJSON(w, http.StatusConflict, errorResponse{Error: "CONFLICT", Message: err.Error()})
case errors.Is(err, service.ErrInternal):
slog.Error("service error", "err", err)
msg := strings.TrimPrefix(err.Error(), "internal error: ")
writeJSON(w, http.StatusInternalServerError, errorResponse{Error: "INTERNAL_ERROR", Message: msg})
writeJSON(w, http.StatusInternalServerError, errorResponse{Error: "INTERNAL_ERROR", Message: "an internal error occurred"})
default:
slog.Error("service error", "err", err)
writeJSON(w, http.StatusInternalServerError, errorResponse{Error: "INTERNAL_ERROR", Message: "internal error"})
+6 -6
View File
@@ -155,8 +155,8 @@ func TestCreateInvite_CreateInviteFailure(t *testing.T) {
if err := json.NewDecoder(rr.Body).Decode(&resp); err != nil {
t.Fatalf("decode response: %v", err)
}
if resp["message"] != "failed to create invite" {
t.Errorf("message = %v, want failed to create invite", resp["message"])
if resp["message"] != "an internal error occurred" {
t.Errorf("message = %v, want an internal error occurred", resp["message"])
}
}
@@ -185,8 +185,8 @@ func TestCreateInvite_GetInviteFailure(t *testing.T) {
if err := json.NewDecoder(rr.Body).Decode(&resp); err != nil {
t.Fatalf("decode response: %v", err)
}
if resp["message"] != "failed to retrieve invite" {
t.Errorf("message = %v, want failed to retrieve invite", resp["message"])
if resp["message"] != "an internal error occurred" {
t.Errorf("message = %v, want an internal error occurred", resp["message"])
}
if _, err := database.Exec(`DROP TRIGGER delete_invite_after_insert`); err != nil {
t.Fatalf("drop trigger: %v", err)
@@ -387,8 +387,8 @@ func TestRevokeInvite_RevokeFailure(t *testing.T) {
if err := json.NewDecoder(rr2.Body).Decode(&resp); err != nil {
t.Fatalf("decode revoke failure response: %v", err)
}
if resp["message"] != "failed to revoke invite" {
t.Errorf("message = %v, want failed to revoke invite", resp["message"])
if resp["message"] != "an internal error occurred" {
t.Errorf("message = %v, want an internal error occurred", resp["message"])
}
}
+2
View File
@@ -11,6 +11,7 @@ import (
"fmt"
"io"
"os"
"strings"
"github.com/tetratelabs/wazero"
"github.com/tetratelabs/wazero/api"
@@ -76,6 +77,7 @@ func (r *Registry) activateWithRuntime(ctx context.Context, inst *Instance) erro
modCfg := wazero.NewModuleConfig().
WithName(inst.Manifest.Name).
WithStdin(strings.NewReader("")). // prevent plugins from reading server stdin
WithStdout(io.Discard).
WithStderr(io.Discard).
WithStartFunctions() // suppress _start; exports are called on demand