Skip to content

Commit 8e9ab85

Browse files
mattwompletwifkak
authored andcommitted
signer: Avoid mutating response until signing (#291)
After successfully (200) fetching an AMP document, complete all validation steps that could result in proxying the original response before mutating any headers.
1 parent c84a503 commit 8e9ab85

File tree

2 files changed

+94
-17
lines changed

2 files changed

+94
-17
lines changed

packager/signer/signer.go

Lines changed: 37 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -348,17 +348,16 @@ func (this *Signer) ServeHTTP(resp http.ResponseWriter, req *http.Request, param
348348
proxy(resp, fetchResp, nil)
349349
return
350350
}
351+
var act string
351352
var transformVersion int64
352353
if this.requireHeaders {
353354
header_value := GetJoined(req.Header, "AMP-Cache-Transform")
354-
var act string
355355
act, transformVersion = amp_cache_transform.ShouldSendSXG(header_value)
356356
if act == "" {
357357
log.Println("Not packaging because AMP-Cache-Transform request header is invalid:", header_value)
358358
proxy(resp, fetchResp, nil)
359359
return
360360
}
361-
resp.Header().Set("AMP-Cache-Transform", act)
362361
} else {
363362
var err error
364363
transformVersion, err = transformer.SelectVersion(nil)
@@ -387,17 +386,8 @@ func (this *Signer) ServeHTTP(resp http.ResponseWriter, req *http.Request, param
387386
proxy(resp, fetchResp, nil)
388387
return
389388
}
390-
fetchResp.Header.Del(header)
391389
}
392390

393-
// Mutate the fetched CSP to make sure it cannot break AMP pages.
394-
fetchResp.Header.Set(
395-
"Content-Security-Policy",
396-
MutateFetchedContentSecurityPolicy(
397-
fetchResp.Header.Get("Content-Security-Policy")))
398-
399-
fetchResp.Header.Del("Link") // Ensure there are no privacy-violating Link:rel=preload headers.
400-
401391
if fetchResp.Header.Get("Variants") != "" || fetchResp.Header.Get("Variant-Key") != "" ||
402392
// Include versioned headers per https://github.com/WICG/webpackage/pull/406.
403393
fetchResp.Header.Get("Variants-04") != "" || fetchResp.Header.Get("Variant-Key-04") != "" {
@@ -408,7 +398,7 @@ func (this *Signer) ServeHTTP(resp http.ResponseWriter, req *http.Request, param
408398
return
409399
}
410400

411-
this.serveSignedExchange(resp, fetchResp, signURL, transformVersion)
401+
this.serveSignedExchange(resp, fetchResp, signURL, act, transformVersion)
412402

413403
case 304:
414404
// If fetchURL returns a 304, then also return a 304 with appropriate headers.
@@ -451,9 +441,7 @@ func formatLinkHeader(preloads []*rpb.Metadata_Preload) (string, error) {
451441
}
452442

453443
// serveSignedExchange does the actual work of transforming, packaging and signed and writing to the response.
454-
func (this *Signer) serveSignedExchange(resp http.ResponseWriter, fetchResp *http.Response, signURL *url.URL, transformVersion int64) {
455-
fetchResp.Header.Set("X-Content-Type-Options", "nosniff")
456-
444+
func (this *Signer) serveSignedExchange(resp http.ResponseWriter, fetchResp *http.Response, signURL *url.URL, act string, transformVersion int64) {
457445
// After this, fetchResp.Body is consumed, and attempts to read or proxy it will result in an empty body.
458446
fetchBody, err := ioutil.ReadAll(io.LimitReader(fetchResp.Body, maxBodyLength))
459447
if err != nil {
@@ -470,17 +458,43 @@ func (this *Signer) serveSignedExchange(resp http.ResponseWriter, fetchResp *htt
470458
proxy(resp, fetchResp, fetchBody)
471459
return
472460
}
473-
fetchResp.Header.Set("Content-Length", strconv.Itoa(len(transformed)))
461+
462+
// Validate and format Link header.
474463
linkHeader, err := formatLinkHeader(metadata.Preloads)
475464
if err != nil {
476465
log.Println("Not packaging due to Link header error:", err)
477466
proxy(resp, fetchResp, fetchBody)
478467
return
479468
}
469+
470+
// Begin mutations on original fetch response. From this point forward, do
471+
// not fall-back to proxy().
472+
473+
// Remove stateful headers.
474+
for header := range statefulResponseHeaders {
475+
fetchResp.Header.Del(header)
476+
}
477+
478+
// Set Link header if formatting returned a valid value, otherwise, delete
479+
// it to ensure there are no privacy-violating Link:rel=preload headers.
480480
if linkHeader != "" {
481481
fetchResp.Header.Set("Link", linkHeader)
482+
} else {
483+
fetchResp.Header.Del("Link")
482484
}
483485

486+
// Set content length.
487+
fetchResp.Header.Set("Content-Length", strconv.Itoa(len(transformed)))
488+
489+
// Set general security headers.
490+
fetchResp.Header.Set("X-Content-Type-Options", "nosniff")
491+
492+
// Mutate the fetched CSP to make sure it cannot break AMP pages.
493+
fetchResp.Header.Set(
494+
"Content-Security-Policy",
495+
MutateFetchedContentSecurityPolicy(
496+
fetchResp.Header.Get("Content-Security-Policy")))
497+
484498
exchange := signedexchange.NewExchange(
485499
accept.SxgVersion, /*uri=*/signURL.String(), /*method=*/"GET",
486500
http.Header{}, fetchResp.StatusCode, fetchResp.Header, []byte(transformed))
@@ -520,6 +534,13 @@ func (this *Signer) serveSignedExchange(resp http.ResponseWriter, fetchResp *htt
520534
util.NewHTTPError(http.StatusInternalServerError, "Error serializing exchange: ", err).LogAndRespond(resp)
521535
}
522536

537+
// If requireHeaders was true when constructing signer, the
538+
// AMP-Cache-Transform outer response header is required (and has already
539+
// been validated)
540+
if (act != "") {
541+
resp.Header().Set("AMP-Cache-Transform", act)
542+
}
543+
523544
// TODO(twifkak): Add Cache-Control: public with expiry to match when we think the AMP Cache
524545
// should fetch an update (half-way between signature date & expires).
525546
resp.Header().Set("Content-Type", accept.SxgContentType)

packager/signer/signer_test.go

Lines changed: 57 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -171,7 +171,6 @@ func (this *SignerSuite) TestSimple() {
171171
[]string{"content-encoding", "content-length", "content-security-policy", "content-type", "date", "digest", "x-content-type-options"},
172172
headerNames(exchange.ResponseHeaders))
173173
this.Assert().Equal("text/html", exchange.ResponseHeaders.Get("Content-Type"))
174-
this.Assert().Equal("text/html", exchange.ResponseHeaders.Get("Content-Type"))
175174
this.Assert().Equal("nosniff", exchange.ResponseHeaders.Get("X-Content-Type-Options"))
176175
this.Assert().Contains(exchange.SignatureHeaderValue, "validity-url=\""+this.httpSignURL()+"/amppkg/validity\"")
177176
this.Assert().Contains(exchange.SignatureHeaderValue, "integrity=\"digest/mi-sha256-03\"")
@@ -462,6 +461,20 @@ func (this *SignerSuite) TestProxyUnsignedIfMissingAMPCacheTransformHeader() {
462461
this.Assert().Equal(fakeBody, body, "incorrect body: %#v", resp)
463462
}
464463

464+
func (this *SignerSuite) TestProxyUnsignedIfInvalidAMPCacheTransformHeader() {
465+
urlSets := []util.URLSet{{
466+
Sign: &util.URLPattern{[]string{"https"}, "", this.httpsHost(), stringPtr("/amp/.*"), []string{}, stringPtr(""), false, 2000, nil},
467+
}}
468+
resp := pkgt.GetH(this.T(), this.new(urlSets), "/priv/doc?sign="+url.QueryEscape(this.httpsURL()+fakePath), http.Header{
469+
"Accept": {"application/signed-exchange;v=" + accept.AcceptedSxgVersion},
470+
"AMP-Cache-Transform": {"donotmatch"},
471+
})
472+
this.Assert().Equal(http.StatusOK, resp.StatusCode, "incorrect status: %#v", resp)
473+
body, err := ioutil.ReadAll(resp.Body)
474+
this.Require().NoError(err)
475+
this.Assert().Equal(fakeBody, body, "incorrect body: %#v", resp)
476+
}
477+
465478
func (this *SignerSuite) TestProxyUnsignedIfMissingAcceptHeader() {
466479
urlSets := []util.URLSet{{
467480
Sign: &util.URLPattern{[]string{"https"}, "", this.httpsHost(), stringPtr("/amp/.*"), []string{}, stringPtr(""), false, 2000, nil},
@@ -609,6 +622,49 @@ func (this *SignerSuite) TestProxyTransformError() {
609622
this.Assert().Equal(fakeBody, body, "incorrect body: %#v", resp)
610623
}
611624

625+
func (this *SignerSuite) TestProxyHeadersUnaltered() {
626+
urlSets := []util.URLSet{{
627+
Sign: &util.URLPattern{[]string{"https"}, "", this.httpsHost(), stringPtr("/amp/.*"), []string{}, stringPtr(""), false, 2000, nil},
628+
}}
629+
630+
// "Perform local transformations" is close to the last opportunity that a
631+
// response could be proxied instead of signed. Intentionally cause an error
632+
// to occur so that we can verify the proxy response has not been altered.
633+
getTransformerRequest = func(r *rtv.RTVCache, s, u string) *rpb.Request {
634+
return &rpb.Request{Html: string(s), DocumentUrl: u, Config: rpb.Request_CUSTOM,
635+
AllowedFormats: []rpb.Request_HtmlFormat{rpb.Request_AMP},
636+
Transformers: []string{"bogus"}}
637+
}
638+
639+
originalHeaders := map[string]string {
640+
"Content-Type": "text/html",
641+
"Set-Cookie": "chocolate chip",
642+
"Cache-Control": "max-age=31536000",
643+
"Content-Length": fmt.Sprintf("%d", len(fakeBody)),
644+
}
645+
646+
this.fakeHandler = func(resp http.ResponseWriter, req *http.Request) {
647+
for key, value := range originalHeaders {
648+
resp.Header().Set(key, value)
649+
}
650+
resp.Write(fakeBody)
651+
}
652+
resp := this.get(this.T(), this.new(urlSets), "/priv/doc?sign="+url.QueryEscape(this.httpsURL()+fakePath))
653+
this.Assert().Equal(200, resp.StatusCode)
654+
655+
// Compare the final headers to the originals, removing each one after
656+
// checking, so that we can finally verify that no additional headers were
657+
// appended.
658+
for key, value := range originalHeaders {
659+
this.Assert().Equal([]string{value}, resp.Header[key])
660+
resp.Header.Del(key)
661+
}
662+
this.Assert().Equal([]string{"Accept, AMP-Cache-Transform"}, resp.Header["Vary"])
663+
resp.Header.Del("Vary")
664+
resp.Header.Del("Date") // Date header is not tested; it may be updated.
665+
this.Assert().Empty(resp.Header)
666+
}
667+
612668
func TestSignerSuite(t *testing.T) {
613669
suite.Run(t, new(SignerSuite))
614670
}

0 commit comments

Comments
 (0)