Skip to content

Commit 0ffab59

Browse files
authored
fix(config): harden CORS, addons, CSS, and artifact scans (#756)
1 parent 6ddbdee commit 0ffab59

18 files changed

Lines changed: 724 additions & 69 deletions

File tree

addons/tailwind/tailwind.go

Lines changed: 29 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -75,9 +75,10 @@ func (p processor) ProcessCSS(context gowdk.CSSContext) (gowdk.CSSResult, error)
7575
defer os.RemoveAll(tempDir)
7676

7777
tempOutput := filepath.Join(tempDir, "app.css")
78-
input := options.Input
78+
workingDir := cssWorkingDir(context)
79+
input := resolveCSSPath(workingDir, options.Input)
7980
if len(context.Sources) > 0 {
80-
generatedInput, err := writeInputWithSources(tempDir, options.Input, context.Sources)
81+
generatedInput, err := writeInputWithSources(tempDir, workingDir, input, context.Sources)
8182
if err != nil {
8283
return gowdk.CSSResult{}, err
8384
}
@@ -91,6 +92,7 @@ func (p processor) ProcessCSS(context gowdk.CSSContext) (gowdk.CSSResult, error)
9192
var stdout bytes.Buffer
9293
var stderr bytes.Buffer
9394
command := exec.Command(commandPath, args...)
95+
command.Dir = workingDir
9496
command.Stdout = &stdout
9597
command.Stderr = &stderr
9698
if err := command.Run(); err != nil {
@@ -111,11 +113,8 @@ func (p processor) ProcessCSS(context gowdk.CSSContext) (gowdk.CSSResult, error)
111113
}, nil
112114
}
113115

114-
func writeInputWithSources(tempDir string, input string, sources []gowdk.CSSSource) (string, error) {
115-
inputPath, err := filepath.Abs(input)
116-
if err != nil {
117-
return "", err
118-
}
116+
func writeInputWithSources(tempDir string, workingDir string, input string, sources []gowdk.CSSSource) (string, error) {
117+
inputPath := resolveCSSPath(workingDir, input)
119118
relInput, err := filepath.Rel(tempDir, inputPath)
120119
if err != nil {
121120
return "", err
@@ -130,10 +129,7 @@ func writeInputWithSources(tempDir string, input string, sources []gowdk.CSSSour
130129
continue
131130
}
132131
seen[sourcePath] = true
133-
absoluteSource, err := filepath.Abs(sourcePath)
134-
if err != nil {
135-
return "", err
136-
}
132+
absoluteSource := resolveCSSPath(workingDir, sourcePath)
137133
relativeSource, err := filepath.Rel(tempDir, absoluteSource)
138134
if err != nil {
139135
return "", err
@@ -148,6 +144,28 @@ func writeInputWithSources(tempDir string, input string, sources []gowdk.CSSSour
148144
return generatedInput, nil
149145
}
150146

147+
func cssWorkingDir(context gowdk.CSSContext) string {
148+
for _, candidate := range []string{context.WorkingDir, context.ConfigDir, context.ProjectRoot, context.SourceRoot} {
149+
candidate = strings.TrimSpace(candidate)
150+
if candidate != "" {
151+
return candidate
152+
}
153+
}
154+
return "."
155+
}
156+
157+
func resolveCSSPath(workingDir string, path string) string {
158+
path = strings.TrimSpace(path)
159+
if filepath.IsAbs(path) {
160+
return path
161+
}
162+
absolute, err := filepath.Abs(filepath.Join(workingDir, path))
163+
if err != nil {
164+
return filepath.Join(workingDir, path)
165+
}
166+
return absolute
167+
}
168+
151169
func cssPath(path string) string {
152170
path = filepath.ToSlash(path)
153171
path = strings.ReplaceAll(path, `\`, `\\`)

addons/tailwind/tailwind_test.go

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,8 +38,10 @@ func TestProcessCSSRunsStandaloneCommand(t *testing.T) {
3838
t.Fatal(err)
3939
}
4040
argsFile := filepath.Join(root, "args.txt")
41+
cwdFile := filepath.Join(root, "cwd.txt")
4142
inputCopy := filepath.Join(root, "generated-input.css")
4243
t.Setenv("TAILWIND_ARGS_FILE", argsFile)
44+
t.Setenv("TAILWIND_CWD_FILE", cwdFile)
4345
t.Setenv("TAILWIND_INPUT_COPY", inputCopy)
4446

4547
result, err := Addon(Options{
@@ -66,6 +68,17 @@ func TestProcessCSSRunsStandaloneCommand(t *testing.T) {
6668
if len(result.Stylesheets) != 1 || result.Stylesheets[0].Href != "/assets/site.css" {
6769
t.Fatalf("unexpected stylesheets: %#v", result.Stylesheets)
6870
}
71+
wantCWD, err := os.Getwd()
72+
if err != nil {
73+
t.Fatal(err)
74+
}
75+
cwd, err := os.ReadFile(cwdFile)
76+
if err != nil {
77+
t.Fatal(err)
78+
}
79+
if strings.TrimSpace(string(cwd)) != wantCWD {
80+
t.Fatalf("expected tailwind command to run in %q, got %q", wantCWD, cwd)
81+
}
6982

7083
args, err := os.ReadFile(argsFile)
7184
if err != nil {
@@ -89,6 +102,63 @@ func TestProcessCSSRunsStandaloneCommand(t *testing.T) {
89102
}
90103
}
91104

105+
func TestProcessCSSResolvesRelativePathsFromContextWorkingDir(t *testing.T) {
106+
root := t.TempDir()
107+
if err := os.Mkdir(filepath.Join(root, "styles"), 0o755); err != nil {
108+
t.Fatal(err)
109+
}
110+
input := filepath.Join(root, "styles", "app.css")
111+
if err := os.WriteFile(input, []byte(`@import "tailwindcss";`), 0o644); err != nil {
112+
t.Fatal(err)
113+
}
114+
argsFile := filepath.Join(root, "args.txt")
115+
cwdFile := filepath.Join(root, "cwd.txt")
116+
inputCopy := filepath.Join(root, "generated-input.css")
117+
t.Setenv("TAILWIND_ARGS_FILE", argsFile)
118+
t.Setenv("TAILWIND_CWD_FILE", cwdFile)
119+
t.Setenv("TAILWIND_INPUT_COPY", inputCopy)
120+
121+
other := t.TempDir()
122+
previous, err := os.Getwd()
123+
if err != nil {
124+
t.Fatal(err)
125+
}
126+
if err := os.Chdir(other); err != nil {
127+
t.Fatal(err)
128+
}
129+
t.Cleanup(func() {
130+
if err := os.Chdir(previous); err != nil {
131+
t.Fatal(err)
132+
}
133+
})
134+
135+
_, err = Addon(Options{
136+
Input: "styles/app.css",
137+
Command: fakeTailwindCommand(t),
138+
}).ProcessCSS(gowdk.CSSContext{
139+
WorkingDir: root,
140+
Sources: []gowdk.CSSSource{{Path: "site.page.gwdk"}},
141+
})
142+
if err != nil {
143+
t.Fatal(err)
144+
}
145+
cwd, err := os.ReadFile(cwdFile)
146+
if err != nil {
147+
t.Fatal(err)
148+
}
149+
if strings.TrimSpace(string(cwd)) != root {
150+
t.Fatalf("expected tailwind command to run in %q, got %q", root, cwd)
151+
}
152+
generatedInput, err := os.ReadFile(inputCopy)
153+
if err != nil {
154+
t.Fatal(err)
155+
}
156+
generated := string(generatedInput)
157+
if !strings.Contains(generated, `styles/app.css`) {
158+
t.Fatalf("expected generated input to reference context-relative input, got:\n%s", generated)
159+
}
160+
}
161+
92162
func TestProcessCSSReportsMissingExecutable(t *testing.T) {
93163
input := filepath.Join(t.TempDir(), "app.css")
94164
if err := os.WriteFile(input, []byte(`@import "tailwindcss";`), 0o644); err != nil {
@@ -195,6 +265,9 @@ func fakeTailwindCommand(t *testing.T) string {
195265

196266
const fakeTailwindScript = `#!/bin/sh
197267
set -eu
268+
if [ "${TAILWIND_CWD_FILE:-}" != "" ]; then
269+
pwd > "$TAILWIND_CWD_FILE"
270+
fi
198271
printf '%s\n' "$@" > "$TAILWIND_ARGS_FILE"
199272
out=""
200273
in=""

docs/engineering/security.md

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -66,15 +66,21 @@ diagnostic code, a `file:line`, and remediation; run `gowdk explain <code>` for
6666
details.
6767

6868
`gowdk build` evaluates the same static baseline before writing output and scans
69-
the final emitted artifact files for bundled secrets after generation.
69+
text-like final emitted artifact files for bundled secrets after generation. The
70+
final scan is bounded to 1 MiB per text file and 16 MiB total text input; known
71+
binary artifact types such as WASM, images, archives, and generated binaries are
72+
not scanned as text, and oversized text artifacts produce warnings.
73+
7074
Production builds fail on error-severity findings unless they are explicitly
7175
waived or scoped-bypassed (see below); non-production builds print a prominent
72-
warning summary without blocking local iteration. `gowdk audit` remains the
73-
explicit report and CI surface: it prints the full human/JSON report, reads
74-
declared `*.audit.gwdk` policies, checks frontend risks such as bundle secrets
75-
and raw-HTML sinks, can emit readable standalone runtime tests with `gowdk audit
76-
--emit-tests`, can verify committed tests are current with `gowdk audit
77-
--check-tests`, and can run generated-app runtime tests with `gowdk audit --run`.
76+
warning summary without blocking local iteration.
77+
78+
`gowdk audit` remains the explicit report and CI surface: it prints the full
79+
human/JSON report, reads declared `*.audit.gwdk` policies, checks frontend risks
80+
such as bundle secrets and raw-HTML sinks, can emit readable standalone runtime
81+
tests with `gowdk audit --emit-tests`, can verify committed tests are current
82+
with `gowdk audit --check-tests`, and can run generated-app runtime tests with
83+
`gowdk audit --run`.
7884

7985
## CI-Native Output: JSON Schema, SARIF, Fingerprints, And Diff
8086

docs/reference/cli.md

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -100,10 +100,12 @@ gowdk lsp [--config <file>] [--project-root <dir>] [--ssr]
100100
(`GOPROXY=off`, `GOTOOLCHAIN=local`). A failed expectation is reported as
101101
`audit_test_failed`; exceeding the deadline is reported distinctly as
102102
`audit_test_timeout`.
103-
`gowdk build` runs the same baseline gate before writing output, scans the
104-
final emitted artifact files for bundled secrets after generation, blocks
105-
production builds on error-severity findings unless `--allow-insecure` is set,
106-
and writes the
103+
`gowdk build` runs the same baseline gate before writing output, scans
104+
text-like final emitted artifact files for bundled secrets after generation
105+
with a 1 MiB per-file limit and 16 MiB total text scan budget, skips known
106+
binary artifact types such as WASM, images, archives, and binaries, reports
107+
oversized text artifacts as warnings, blocks production builds on
108+
error-severity findings unless `--allow-insecure` is set, and writes the
107109
posture alone to a non-served `.gowdk/reports/<output-name>/gowdk-security.json`
108110
path outside the selected output directory.
109111
`gowdk audit` exit codes form a stable CI contract; gate on the specific code

docs/reference/config.md

Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -273,11 +273,18 @@ type CORSConfig struct {
273273
```
274274

275275
`AllowedOrigins` accepts literal `http` or `https` origins, or `"*"` only when
276-
`AllowCredentials` is false. Preflight requests must match a generated
277-
API/command/query route and must request a method and headers allowed by the
278-
policy. `AllowedMethods` is optional; when omitted, the matched route method is
279-
returned. `AllowedHeaders` must include non-simple request headers such as
280-
`Content-Type` for JSON APIs and any configured CSRF header.
276+
`AllowCredentials` is false. Origin validation canonicalizes host case, strips
277+
explicit default ports (`http:80`, `https:443`), strips a trailing DNS dot, and
278+
handles bracketed IPv6 hosts. Unicode hostnames are rejected; use ASCII
279+
punycode. Invalid ports, userinfo, paths, query strings, and fragments are
280+
rejected with the offending origin in the diagnostic. Runtime request matching
281+
uses the same canonical form as config validation.
282+
283+
Preflight requests must match a generated API/command/query route and must
284+
request a method and headers allowed by the policy. `AllowedMethods` is
285+
optional; when omitted, the matched route method is returned. `AllowedHeaders`
286+
must include non-simple request headers such as `Content-Type` for JSON APIs and
287+
any configured CSRF header.
281288

282289
`.gwdk` API declarations can attach endpoint-local CORS with a trailing `cors`
283290
clause:
@@ -732,6 +739,13 @@ output. Current validation uses feature registration for render-mode,
732739
realtime, and other compiler checks; SPA builds invoke addons that implement
733740
`gowdk.CSSProcessor` or `gowdk.SEOProvider`.
734741

742+
Config loading rejects nil addons, empty addon names, duplicate addon names,
743+
empty feature IDs, addons with no features, and duplicate feature ownership
744+
except for the legacy `spa`/`static` `FeatureSPA` alias overlap. External addon
745+
feature names are allowed when they are non-empty. Behaviorful built-in features
746+
must provide their matching extension contract: `FeatureSEO` requires
747+
`gowdk.SEOProvider`, and `FeatureAuth` requires `gowdk.AuthSessionProvider`.
748+
735749
DB helper usage is ordinary Go code around `database/sql`; see [db.md](db.md)
736750
for migrations, transactions, readiness, and sqlc usage.
737751

docs/reference/css.md

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -230,6 +230,9 @@ type CSSProcessor interface {
230230

231231
`CSSContext` includes:
232232

233+
- `ProjectRoot`, `ConfigDir`, `SourceRoot`, and `WorkingDir`: explicit root
234+
paths for processors that need deterministic relative path handling. In CLI
235+
builds these point at the loaded project/config root.
233236
- `Sources`: discovered page/component source metadata.
234237
- `Sources[*].CSSClasses`: extracted literal class names from the current view
235238
subset.
@@ -296,7 +299,9 @@ Defaults:
296299
At build time the addon creates a temporary Tailwind input file that imports the
297300
configured `Input` CSS and adds `@source` declarations for discovered GOWDK
298301
source files. This follows Tailwind v4's CSS/source directive model. It then
299-
runs the standalone executable with `-i <temp-input> -o <temp-output>`.
302+
runs the standalone executable with `-i <temp-input> -o <temp-output>` from the
303+
CSS context working directory. Relative `Input` and source paths resolve from
304+
that directory instead of the caller's process working directory.
300305

301306
The addon does not use npm, run `npx`, or run through a shell. If `Command` is
302307
omitted and `tailwindcss` is not available on `PATH`, `gowdk build` fails with

0 commit comments

Comments
 (0)