@nestia/core 13.0.0 → 13.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
|
@@ -58,7 +58,7 @@ function EncryptedBody(validator) {
|
|
|
58
58
|
? param({ headers: headers.get(), body, direction: "decode" })
|
|
59
59
|
: param;
|
|
60
60
|
// PARSE AND VALIDATE DATA
|
|
61
|
-
const data =
|
|
61
|
+
const data = decode(body, password.key, password.iv);
|
|
62
62
|
const error = checker(data);
|
|
63
63
|
if (error !== null)
|
|
64
64
|
throw error;
|
|
@@ -66,10 +66,28 @@ function EncryptedBody(validator) {
|
|
|
66
66
|
});
|
|
67
67
|
})();
|
|
68
68
|
}
|
|
69
|
-
/**
|
|
70
|
-
|
|
69
|
+
/**
|
|
70
|
+
* Decrypt and JSON-parse the request body behind a single failure response.
|
|
71
|
+
*
|
|
72
|
+
* `AesPkcs5` is AES-CBC without an authentication tag, so a MITM attacker can
|
|
73
|
+
* forge ciphertexts. Splitting the two failure modes of a forged body — a bad
|
|
74
|
+
* PKCS#5 padding (decryption throws) versus valid padding whose plaintext is
|
|
75
|
+
* not JSON (`JSON.parse` throws) — into two different HTTP status codes turns
|
|
76
|
+
* this decorator into a padding oracle: one observable bit per request lets the
|
|
77
|
+
* attacker recover the plaintext byte by byte without the key
|
|
78
|
+
* (GHSA-pqj4-gvf7-6fq5).
|
|
79
|
+
*
|
|
80
|
+
* Both failure modes must therefore collapse into one indistinguishable
|
|
81
|
+
* `BadRequestException` (identical status, message, and body). Only the
|
|
82
|
+
* downstream type validation of an already-parsed body may report its own
|
|
83
|
+
* distinct error, because reaching it requires plaintext the attacker cannot
|
|
84
|
+
* forge without already knowing it.
|
|
85
|
+
*
|
|
86
|
+
* @internal
|
|
87
|
+
*/
|
|
88
|
+
const decode = (body, key, iv) => {
|
|
71
89
|
try {
|
|
72
|
-
return AesPkcs5_1.AesPkcs5.decrypt(body, key, iv);
|
|
90
|
+
return JSON.parse(AesPkcs5_1.AesPkcs5.decrypt(body, key, iv));
|
|
73
91
|
}
|
|
74
92
|
catch (exp) {
|
|
75
93
|
if (exp instanceof Error)
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"EncryptedBody.js","sourceRoot":"","sources":["../../src/decorators/EncryptedBody.ts"],"names":[],"mappings":";;;;;;;;;;;;AACA,2DAAwD;AACxD,2CAIwB;AAKxB,kDAA+C;AAC/C,oEAAuE;AACvE,4DAAyD;AACzD,oEAAiE;AACjE,4DAAyD;AACzD,4EAAyE;AAEzE;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,uBACE,SAAoC;IAEpC,MAAM,OAAO,GAAG,IAAA,6CAAqB,EAAC,eAAe,CAAC,CAAC,SAAS,CAAC,CAAC;IAClE,OAAO,IAAA,6BAAoB,EAAC,SAAe,aAAa,CACtD,QAAa,EACb,OAAyB;;YAEzB,MAAM,OAAO,GAAqC,OAAO;iBACtD,YAAY,EAAE;iBACd,UAAU,EAAE,CAAC;YAChB,IAAI,IAAA,6BAAa,EAAC,OAAO,CAAC,OAAO,CAAC,cAAc,CAAC,EAAE,YAAY,CAAC,KAAK,KAAK;gBACxE,MAAM,IAAI,4BAAmB,CAAC,wCAAwC,CAAC,CAAC;YAE1E,MAAM,KAAK,GACT,OAAO,CAAC,WAAW,CAAC,2CAAuB,EAAE,OAAO,CAAC,QAAQ,EAAE,CAAC,CAAC;YACnE,IAAI,CAAC,KAAK;gBACR,MAAM,IAAI,KAAK,CACb,wEAAwE,CACzE,CAAC;YAEJ,gBAAgB;YAChB,MAAM,OAAO,GAAsC,IAAI,qBAAS,CAAC,GAAG,EAAE,CACpE,IAAA,qCAAiB,EAAC,OAAO,CAAC,OAAO,CAAC,CACnC,CAAC;YACF,MAAM,IAAI,GAAW,MAAM,IAAA,6BAAa,EAAC,OAAO,CAAC,CAAC;YAClD,MAAM,QAAQ,GACZ,OAAO,KAAK,KAAK,UAAU;gBACzB,CAAC,CAAC,KAAK,CAAC,EAAE,OAAO,EAAE,OAAO,CAAC,GAAG,EAAE,EAAE,IAAI,EAAE,SAAS,EAAE,QAAQ,EAAE,CAAC;gBAC9D,CAAC,CAAC,KAAK,CAAC;YAEZ,0BAA0B;YAC1B,MAAM,IAAI,GAAQ,
|
|
1
|
+
{"version":3,"file":"EncryptedBody.js","sourceRoot":"","sources":["../../src/decorators/EncryptedBody.ts"],"names":[],"mappings":";;;;;;;;;;;;AACA,2DAAwD;AACxD,2CAIwB;AAKxB,kDAA+C;AAC/C,oEAAuE;AACvE,4DAAyD;AACzD,oEAAiE;AACjE,4DAAyD;AACzD,4EAAyE;AAEzE;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,uBACE,SAAoC;IAEpC,MAAM,OAAO,GAAG,IAAA,6CAAqB,EAAC,eAAe,CAAC,CAAC,SAAS,CAAC,CAAC;IAClE,OAAO,IAAA,6BAAoB,EAAC,SAAe,aAAa,CACtD,QAAa,EACb,OAAyB;;YAEzB,MAAM,OAAO,GAAqC,OAAO;iBACtD,YAAY,EAAE;iBACd,UAAU,EAAE,CAAC;YAChB,IAAI,IAAA,6BAAa,EAAC,OAAO,CAAC,OAAO,CAAC,cAAc,CAAC,EAAE,YAAY,CAAC,KAAK,KAAK;gBACxE,MAAM,IAAI,4BAAmB,CAAC,wCAAwC,CAAC,CAAC;YAE1E,MAAM,KAAK,GACT,OAAO,CAAC,WAAW,CAAC,2CAAuB,EAAE,OAAO,CAAC,QAAQ,EAAE,CAAC,CAAC;YACnE,IAAI,CAAC,KAAK;gBACR,MAAM,IAAI,KAAK,CACb,wEAAwE,CACzE,CAAC;YAEJ,gBAAgB;YAChB,MAAM,OAAO,GAAsC,IAAI,qBAAS,CAAC,GAAG,EAAE,CACpE,IAAA,qCAAiB,EAAC,OAAO,CAAC,OAAO,CAAC,CACnC,CAAC;YACF,MAAM,IAAI,GAAW,MAAM,IAAA,6BAAa,EAAC,OAAO,CAAC,CAAC;YAClD,MAAM,QAAQ,GACZ,OAAO,KAAK,KAAK,UAAU;gBACzB,CAAC,CAAC,KAAK,CAAC,EAAE,OAAO,EAAE,OAAO,CAAC,GAAG,EAAE,EAAE,IAAI,EAAE,SAAS,EAAE,QAAQ,EAAE,CAAC;gBAC9D,CAAC,CAAC,KAAK,CAAC;YAEZ,0BAA0B;YAC1B,MAAM,IAAI,GAAQ,MAAM,CAAC,IAAI,EAAE,QAAQ,CAAC,GAAG,EAAE,QAAQ,CAAC,EAAE,CAAC,CAAC;YAC1D,MAAM,KAAK,GAAiB,OAAO,CAAC,IAAI,CAAC,CAAC;YAC1C,IAAI,KAAK,KAAK,IAAI;gBAAE,MAAM,KAAK,CAAC;YAChC,OAAO,IAAI,CAAC;QACd,CAAC;KAAA,CAAC,EAAE,CAAC;AACP,CAAC;AAED;;;;;;;;;;;;;;;;;;GAkBG;AACH,MAAM,MAAM,GAAG,CAAC,IAAY,EAAE,GAAW,EAAE,EAAU,EAAW,EAAE;IAChE,IAAI,CAAC;QACH,OAAO,IAAI,CAAC,KAAK,CAAC,mBAAQ,CAAC,OAAO,CAAC,IAAI,EAAE,GAAG,EAAE,EAAE,CAAC,CAAC,CAAC;IACrD,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,IAAI,GAAG,YAAY,KAAK;YACtB,MAAM,IAAI,4BAAmB,CAC3B,qFAAqF,CACtF,CAAC;;YACC,MAAM,GAAG,CAAC;IACjB,CAAC;AACH,CAAC,CAAC"}
|
|
@@ -8,6 +8,7 @@ import (
|
|
|
8
8
|
"io"
|
|
9
9
|
"os"
|
|
10
10
|
"path/filepath"
|
|
11
|
+
"sync"
|
|
11
12
|
"time"
|
|
12
13
|
|
|
13
14
|
shimcompiler "github.com/microsoft/typescript-go/shim/compiler"
|
|
@@ -68,23 +69,54 @@ func runBuild(args []string) int {
|
|
|
68
69
|
return 2
|
|
69
70
|
}
|
|
70
71
|
if len(diags) > 0 {
|
|
72
|
+
if prog != nil {
|
|
73
|
+
prog.Close()
|
|
74
|
+
}
|
|
71
75
|
driver.WritePrettyDiagnostics(stderr, diags, cwd)
|
|
72
76
|
return 2
|
|
73
77
|
}
|
|
74
|
-
defer prog.Close()
|
|
75
|
-
releaseTypiaRegistries := registerTypiaDefaultLibraryClassifier(prog)
|
|
76
|
-
defer releaseTypiaRegistries()
|
|
77
78
|
if profile {
|
|
78
79
|
started = time.Now()
|
|
79
80
|
}
|
|
80
81
|
if diags := prog.Diagnostics(); len(diags) > 0 {
|
|
81
82
|
profileBuildStep(profile, "diagnostics", started)
|
|
83
|
+
prog.Close()
|
|
82
84
|
driver.WritePrettyDiagnostics(stderr, diags, cwd)
|
|
83
85
|
return 2
|
|
84
86
|
}
|
|
85
87
|
profileBuildStep(profile, "diagnostics", started)
|
|
86
88
|
|
|
87
89
|
shouldEmit := !prog.ParsedConfig.ParsedConfig.CompilerOptions.NoEmit.IsTrue()
|
|
90
|
+
if !shouldEmit {
|
|
91
|
+
// Plugin transformers are part of tsgo's emit pipeline. Reload with emit
|
|
92
|
+
// enabled so check/noEmit traverses the same source set, then discard every
|
|
93
|
+
// generated output below. The original program already proved the project's
|
|
94
|
+
// diagnostics; diagnosing this private reload would reject valid analysis-
|
|
95
|
+
// only options such as allowImportingTsExtensions.
|
|
96
|
+
prog.Close()
|
|
97
|
+
if profile {
|
|
98
|
+
started = time.Now()
|
|
99
|
+
}
|
|
100
|
+
prog, diags, err = driver.LoadProgram(cwd, *tsconfigPath, driver.LoadProgramOptions{
|
|
101
|
+
ForceEmit: true,
|
|
102
|
+
OutDir: *outDir,
|
|
103
|
+
})
|
|
104
|
+
profileBuildStep(profile, "load-transform-program", started)
|
|
105
|
+
if err != nil {
|
|
106
|
+
fmt.Fprintf(stderr, "ttsc-nestia build: %v\n", err)
|
|
107
|
+
return 2
|
|
108
|
+
}
|
|
109
|
+
if len(diags) > 0 {
|
|
110
|
+
if prog != nil {
|
|
111
|
+
prog.Close()
|
|
112
|
+
}
|
|
113
|
+
driver.WritePrettyDiagnostics(stderr, diags, cwd)
|
|
114
|
+
return 2
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
defer prog.Close()
|
|
118
|
+
releaseTypiaRegistries := registerTypiaDefaultLibraryClassifier(prog)
|
|
119
|
+
defer releaseTypiaRegistries()
|
|
88
120
|
if !*quiet {
|
|
89
121
|
fmt.Fprintf(
|
|
90
122
|
stdout,
|
|
@@ -97,9 +129,6 @@ func runBuild(args []string) int {
|
|
|
97
129
|
shouldEmit,
|
|
98
130
|
)
|
|
99
131
|
}
|
|
100
|
-
if !shouldEmit {
|
|
101
|
-
return 0
|
|
102
|
-
}
|
|
103
132
|
|
|
104
133
|
// AST-integration emit: typia's per-file transformer and @nestia/core's own
|
|
105
134
|
// per-file transformer both run inside tsgo's emit pipeline (sharing the
|
|
@@ -132,10 +161,20 @@ func runBuild(args []string) int {
|
|
|
132
161
|
transforms := append([]driver.PluginTransform{typiaTransform, coreTransform}, contributorTransforms...)
|
|
133
162
|
|
|
134
163
|
emitted := []string{}
|
|
164
|
+
pending := []buildPendingOutput{}
|
|
165
|
+
var writeMu sync.Mutex
|
|
135
166
|
writeFile := shimcompiler.WriteFile(func(fileName, text string, data *shimcompiler.WriteFileData) error {
|
|
136
167
|
_ = data
|
|
137
|
-
|
|
138
|
-
|
|
168
|
+
if shouldEmit {
|
|
169
|
+
// TypeScript-Go emits declarations in parallel. Serialize the shared
|
|
170
|
+
// slices so buffering cannot lose an artifact or race while the
|
|
171
|
+
// declaration workers call this callback concurrently.
|
|
172
|
+
writeMu.Lock()
|
|
173
|
+
defer writeMu.Unlock()
|
|
174
|
+
emitted = append(emitted, fileName)
|
|
175
|
+
pending = append(pending, buildPendingOutput{FileName: fileName, Text: text})
|
|
176
|
+
}
|
|
177
|
+
return nil
|
|
139
178
|
})
|
|
140
179
|
|
|
141
180
|
// Declaration emit: ttsc delegates the whole emit of a transform-plugin
|
|
@@ -145,7 +184,7 @@ func runBuild(args []string) int {
|
|
|
145
184
|
// core runtime transforms never change the public type surface, so the
|
|
146
185
|
// declarations are taken from the pristine program — done before the JS
|
|
147
186
|
// transform runs so it reads the un-mutated AST.
|
|
148
|
-
if prog.ParsedConfig.ParsedConfig.CompilerOptions.Declaration.IsTrue() {
|
|
187
|
+
if shouldEmit && prog.ParsedConfig.ParsedConfig.CompilerOptions.Declaration.IsTrue() {
|
|
149
188
|
if profile {
|
|
150
189
|
started = time.Now()
|
|
151
190
|
}
|
|
@@ -162,10 +201,6 @@ func runBuild(args []string) int {
|
|
|
162
201
|
fmt.Fprintf(stderr, "ttsc-nestia build: emit failed: %v\n", err)
|
|
163
202
|
return 3
|
|
164
203
|
}
|
|
165
|
-
if len(transformDiags) > 0 {
|
|
166
|
-
WriteTypiaTransformDiagnostics(stderr, transformDiags, cwd)
|
|
167
|
-
return 3
|
|
168
|
-
}
|
|
169
204
|
emitHasError := false
|
|
170
205
|
for _, d := range eDiags {
|
|
171
206
|
fmt.Fprintln(stderr, " -", d.String())
|
|
@@ -173,31 +208,48 @@ func runBuild(args []string) int {
|
|
|
173
208
|
emitHasError = true
|
|
174
209
|
}
|
|
175
210
|
}
|
|
211
|
+
if len(transformDiags) > 0 {
|
|
212
|
+
WriteTypiaTransformDiagnostics(stderr, transformDiags, cwd)
|
|
213
|
+
return 3
|
|
214
|
+
}
|
|
176
215
|
if emitHasError {
|
|
177
216
|
return 3
|
|
178
217
|
}
|
|
179
|
-
if
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
218
|
+
if shouldEmit {
|
|
219
|
+
for _, output := range pending {
|
|
220
|
+
if err := driver.DefaultWriteFile(output.FileName, output.Text); err != nil {
|
|
221
|
+
fmt.Fprintf(stderr, "ttsc-nestia build: emit write failed: %v\n", err)
|
|
222
|
+
return 3
|
|
223
|
+
}
|
|
184
224
|
}
|
|
185
|
-
if
|
|
186
|
-
|
|
187
|
-
|
|
225
|
+
if *manifestPath != "" {
|
|
226
|
+
data, err := json.Marshal(emitted)
|
|
227
|
+
if err != nil {
|
|
228
|
+
fmt.Fprintf(stderr, "ttsc-nestia build: manifest marshal failed: %v\n", err)
|
|
229
|
+
return 3
|
|
230
|
+
}
|
|
231
|
+
if err := os.MkdirAll(filepath.Dir(*manifestPath), 0o755); err != nil {
|
|
232
|
+
fmt.Fprintf(stderr, "ttsc-nestia build: manifest mkdir failed: %v\n", err)
|
|
233
|
+
return 3
|
|
234
|
+
}
|
|
235
|
+
if err := os.WriteFile(*manifestPath, data, 0o644); err != nil {
|
|
236
|
+
fmt.Fprintf(stderr, "ttsc-nestia build: manifest write failed: %v\n", err)
|
|
237
|
+
return 3
|
|
238
|
+
}
|
|
188
239
|
}
|
|
189
|
-
if
|
|
190
|
-
fmt.Fprintf(
|
|
191
|
-
return 3
|
|
240
|
+
if !*quiet {
|
|
241
|
+
fmt.Fprintf(stdout, "// ttsc-nestia build: emitted=%d files\n", len(emitted))
|
|
192
242
|
}
|
|
193
243
|
}
|
|
194
|
-
if !*quiet {
|
|
195
|
-
fmt.Fprintf(stdout, "// ttsc-nestia build: emitted=%d files\n", len(emitted))
|
|
196
|
-
}
|
|
197
244
|
profileBuildStep(profile, "total", totalStarted)
|
|
198
245
|
return 0
|
|
199
246
|
}
|
|
200
247
|
|
|
248
|
+
type buildPendingOutput struct {
|
|
249
|
+
FileName string
|
|
250
|
+
Text string
|
|
251
|
+
}
|
|
252
|
+
|
|
201
253
|
// emitDeclarations runs tsgo's standard declaration emitter for the program.
|
|
202
254
|
// tsgo's MarkLinkedReferences pass can nil-panic on some cross-module reference
|
|
203
255
|
// shapes (e.g. a nestia.config.ts that calls NestFactory.create, compiled by the
|
|
@@ -220,39 +272,7 @@ func emitDeclarations(prog *driver.Program, writeFile shimcompiler.WriteFile) {
|
|
|
220
272
|
}
|
|
221
273
|
|
|
222
274
|
func runCheck(args []string) int {
|
|
223
|
-
|
|
224
|
-
fs.SetOutput(stderr)
|
|
225
|
-
tsconfigPath := fs.String("tsconfig", "tsconfig.json", "path to tsconfig.json")
|
|
226
|
-
cwdOverride := fs.String("cwd", "", "override the working directory")
|
|
227
|
-
pluginsJSON := fs.String("plugins-json", "", "ordered ttsc plugin payload")
|
|
228
|
-
if err := fs.Parse(args); err != nil {
|
|
229
|
-
return 2
|
|
230
|
-
}
|
|
231
|
-
if _, err := plugin.ParsePlan(*pluginsJSON); err != nil {
|
|
232
|
-
fmt.Fprintf(stderr, "ttsc-nestia check: %v\n", err)
|
|
233
|
-
return 2
|
|
234
|
-
}
|
|
235
|
-
cwd, ok := resolveCWD("ttsc-nestia check", *cwdOverride)
|
|
236
|
-
if !ok {
|
|
237
|
-
return 2
|
|
238
|
-
}
|
|
239
|
-
prog, diags, err := driver.LoadProgram(cwd, *tsconfigPath, driver.LoadProgramOptions{
|
|
240
|
-
ForceNoEmit: true,
|
|
241
|
-
})
|
|
242
|
-
if err != nil {
|
|
243
|
-
fmt.Fprintf(stderr, "ttsc-nestia check: %v\n", err)
|
|
244
|
-
return 2
|
|
245
|
-
}
|
|
246
|
-
if len(diags) > 0 {
|
|
247
|
-
driver.WritePrettyDiagnostics(stderr, diags, cwd)
|
|
248
|
-
return 2
|
|
249
|
-
}
|
|
250
|
-
defer prog.Close()
|
|
251
|
-
if diags := prog.Diagnostics(); len(diags) > 0 {
|
|
252
|
-
driver.WritePrettyDiagnostics(stderr, diags, cwd)
|
|
253
|
-
return 2
|
|
254
|
-
}
|
|
255
|
-
return 0
|
|
275
|
+
return runBuild(append([]string{"--noEmit"}, args...))
|
|
256
276
|
}
|
|
257
277
|
|
|
258
278
|
type Diagnostic struct {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nestia/core",
|
|
3
|
-
"version": "13.0.
|
|
3
|
+
"version": "13.0.2",
|
|
4
4
|
"description": "Super-fast validation decorators of NestJS",
|
|
5
5
|
"main": "lib/index.js",
|
|
6
6
|
"exports": {
|
|
@@ -45,8 +45,8 @@
|
|
|
45
45
|
},
|
|
46
46
|
"homepage": "https://nestia.io",
|
|
47
47
|
"dependencies": {
|
|
48
|
-
"@typia/interface": "^14.0.
|
|
49
|
-
"@typia/utils": "^14.0.
|
|
48
|
+
"@typia/interface": "^14.0.2",
|
|
49
|
+
"@typia/utils": "^14.0.2",
|
|
50
50
|
"get-function-location": "^2.0.0",
|
|
51
51
|
"glob": "^11.0.3",
|
|
52
52
|
"path-parser": "^6.1.0",
|
|
@@ -54,9 +54,9 @@
|
|
|
54
54
|
"reflect-metadata": ">=0.1.12",
|
|
55
55
|
"rxjs": ">=6.0.3",
|
|
56
56
|
"tgrid": "^1.1.0",
|
|
57
|
-
"typia": "^14.0.
|
|
57
|
+
"typia": "^14.0.2",
|
|
58
58
|
"ws": "^7.5.3",
|
|
59
|
-
"@nestia/fetcher": "^13.0.
|
|
59
|
+
"@nestia/fetcher": "^13.0.2"
|
|
60
60
|
},
|
|
61
61
|
"peerDependencies": {
|
|
62
62
|
"@modelcontextprotocol/sdk": "^1.18.0",
|
|
@@ -65,8 +65,8 @@
|
|
|
65
65
|
"reflect-metadata": ">=0.1.12",
|
|
66
66
|
"rxjs": ">=6.0.3",
|
|
67
67
|
"ttsc": ">=0.19.2",
|
|
68
|
-
"typia": "^14.0.
|
|
69
|
-
"@nestia/fetcher": "^13.0.
|
|
68
|
+
"typia": "^14.0.2",
|
|
69
|
+
"@nestia/fetcher": "^13.0.2"
|
|
70
70
|
},
|
|
71
71
|
"peerDependenciesMeta": {
|
|
72
72
|
"@modelcontextprotocol/sdk": {
|
|
@@ -69,17 +69,35 @@ export function EncryptedBody<T>(
|
|
|
69
69
|
: param;
|
|
70
70
|
|
|
71
71
|
// PARSE AND VALIDATE DATA
|
|
72
|
-
const data: any =
|
|
72
|
+
const data: any = decode(body, password.key, password.iv);
|
|
73
73
|
const error: Error | null = checker(data);
|
|
74
74
|
if (error !== null) throw error;
|
|
75
75
|
return data;
|
|
76
76
|
})();
|
|
77
77
|
}
|
|
78
78
|
|
|
79
|
-
/**
|
|
80
|
-
|
|
79
|
+
/**
|
|
80
|
+
* Decrypt and JSON-parse the request body behind a single failure response.
|
|
81
|
+
*
|
|
82
|
+
* `AesPkcs5` is AES-CBC without an authentication tag, so a MITM attacker can
|
|
83
|
+
* forge ciphertexts. Splitting the two failure modes of a forged body — a bad
|
|
84
|
+
* PKCS#5 padding (decryption throws) versus valid padding whose plaintext is
|
|
85
|
+
* not JSON (`JSON.parse` throws) — into two different HTTP status codes turns
|
|
86
|
+
* this decorator into a padding oracle: one observable bit per request lets the
|
|
87
|
+
* attacker recover the plaintext byte by byte without the key
|
|
88
|
+
* (GHSA-pqj4-gvf7-6fq5).
|
|
89
|
+
*
|
|
90
|
+
* Both failure modes must therefore collapse into one indistinguishable
|
|
91
|
+
* `BadRequestException` (identical status, message, and body). Only the
|
|
92
|
+
* downstream type validation of an already-parsed body may report its own
|
|
93
|
+
* distinct error, because reaching it requires plaintext the attacker cannot
|
|
94
|
+
* forge without already knowing it.
|
|
95
|
+
*
|
|
96
|
+
* @internal
|
|
97
|
+
*/
|
|
98
|
+
const decode = (body: string, key: string, iv: string): unknown => {
|
|
81
99
|
try {
|
|
82
|
-
return AesPkcs5.decrypt(body, key, iv);
|
|
100
|
+
return JSON.parse(AesPkcs5.decrypt(body, key, iv));
|
|
83
101
|
} catch (exp) {
|
|
84
102
|
if (exp instanceof Error)
|
|
85
103
|
throw new BadRequestException(
|