Redirect behavior?
Copy/pasting from https://github.com/WICG/signature-based-sri/issues/44:
@martinthomson:
OK, let me try again. I don't like the idea that
@pathis involved, but I don't see a way around it. The problem with@pathis that you need to establish some sort of expectation, which means that you just eliminated 302 and friends as tools that a server can use. Unless you want to allow signing of redirects...
This is a really good point, thanks for bringing it up. I'd been assuming that @path would refer to the initial URL that was requested (which would prevent redirects along with substitutions of the response body), but now it's not actually clear to me whether that's right. Is "the request target" the request's URL or current URL in Fetch's parlance?
If the latter, I need to think more about reasonable behavior for each intermediate hop. Signing them isn't crazy, though we'd likely want to require the content-location header? Hrm.
HTTP signatures deal with a single exchange, so you wouldn't be able to bind to a pre-redirect URL (which might be on a different host even).
In Fetch parlance, "request target" and "current URL" are the two equivalent concepts.
I don't think that you can rely on origin servers changing a whole lot and "content-location" isn't always available -- or even the right concept -- for a redirect.
My conclusion was that redirects are probably out if you want to include the URL under signature. At least if you want to avoid the sorts of resource swapping that is involved here.
(An idea for a remedy for those swapping attacks is to have requests include a Sec-Nonce field that is signed in the response, but that totally ruins caching. There are no easy answers.)
HTTP signatures deal with a single exchange, so you wouldn't be able to bind to a pre-redirect URL (which might be on a different host even).
In Fetch parlance, "request target" and "current URL" are the two equivalent concepts.
Got it, thanks for the clarification.
I don't think that you can rely on origin servers changing a whole lot and "content-location" isn't always available -- or even the right concept -- for a redirect.
My conclusion was that redirects are probably out if you want to include the URL under signature. At least if you want to avoid the sorts of resource swapping that is involved here.
Indeed, I should have written location (e.g. the server signs the location header along with the current @path as a way of stamping the redirect response, the client verifies the signature before following (and presumably ensures that the entire chain is signed with an acceptable signature if the client has integrity requirements on the request)).
Or we can just punt on redirect responses for any response that includes @path, on the assumption that the server is telling us that the location from which the resource was served is, in fact, important. I think this is your suggestion above.
I'm not sure which of these options is the least complex. They both seem somewhat unfortunate. :)
cc @punkeel, who expressed interest in @path as mitigation for part of Cloudflare's threat model and who might have opinions here.
(An idea for a remedy for those swapping attacks is to have requests include a
Sec-Noncefield that is signed in the response, but that totally ruins caching. There are no easy answers.)
I'm planning to punt on nonces for the moment, but I agree that they could be a useful addition in the future (see https://github.com/WICG/signature-based-sri/issues/41).
For the record: this directly impacts my use case; our <script> tag points to a redirect. We're also NOT going to use @path in the first few iterations, to keep things simpler, then revisit.
No strong opinions, just a few thoughts:
(A)
The accept-signature header must be sent with all requests. The user-agent can't tell what request will return the content, and the server(s) need to know when to serve the signatures (and "which ones", potentially). Is this something the RFC should clarify?
(B)
I'd been assuming that @path would refer to the initial URL that was requested
This creates two weird "problems":
- The server needs to know what path to include in the signature. This isn't part of the request/response itself, and would have to be based on "human knowledge"? Like, in my case, I know my script is
api.jseven if I redirect toapi-9847389789fd7897.js. - An attacker could control the path in the
<script>tag. I can't think of a scenario where this really matters (the server is happy to serve that script+signature already). I think this is what is mentioned in a previous comment.
(C) Subresource Integrity today is only concerned about the content, not how it gets ~there~ it. Signing the intermediate hops feels weird from that perspective?
(D)
I never heard about content-location before, but that doesn't seem that useful here? It looks like it's about content negotiation, not redirects. Naming is hard!
(E)
Just as a data point, not argument: some origin servers redirect to a "dynamic URL", sometimes based on the incoming request. For example, /script.js?abc=def would redirect to /script-v1.2.js?abc=def. This may complicate signatures.
We, for one, used to do that, until we found "better" solutions. :)
(F) NOT requiring signed hops, then requiring them is technically a breaking change. (#40)
a) The accept-signature header can only be sent when the client asserts integrity metadata. In that case, it's reasonable to expect it to be sent to each server along the way, as the user agent can't know a priori whether the server will forward them on to another server or not.
b) These are valid implications of that understanding of how @path would work. Happily(?), that's not how it works: as Martin noted above, @path will be serialized with a value corresponding to the current request URL.
c) Generally, I think you're correct. Clients want a resource signed by a given key-holder, and as long as they get it, they're happy. That said, we've been talking about @path in the context of substitution attacks, in which some piece of content you've signed is substituted for some other piece of content you've signed. Assume you served good.js and bad.js and signed both: in the absence of redirects, @path allows the client to verify that the server thinks they're getting what they asked for. Signing redirects would be one way of allowing servers to say that the target of the redirect is indeed what the client asked for, even though the URL doesn't match.
d) Ignore content-location. It was just a typo on my part that's quite unrelated to this discussion. :) I meant location, as part of a redirect response.
e) That is exactly the scenario I'm interested in, in c) above. That's the "attack" @path could help mitigate.
f) I agree.
Another wrinkle to consider here. If you want to cover a redirect, the obvious thing to do is cover the request target/current request URL and then cover the status code and location field. But consider that the link might have specified a public key that is only available at the resource that it specified. If that resource needs to redirect, then it might be directing the request to a different origin, where there is no guarantee that the key is the same. Do you want to allow the redirect to specify a different signing key that is used to authenticate the next request?
As @martinthomson points out, we're talking about two things above:
-
Can we sign redirects generally? That seems straightforward, as we've described: sign
@statusandlocationin the same way we sign everything else, and deliver a network error rather than a redirect on failure. This seems like a reasonable capability to add in any event, and should be a relatively small extension to the existing profile. -
Should we require redirects to be signed with a matching key if a client requires a public key match? This is certainly possible, and would fairly robustly (in combination with signing
@path) mitigate the risk that a compromised server hands out a response other than the one expected. We could easily implement this. The questions I have here is entirely practical: could deployments support this requirement, and should it be optional? Hopefully folks like @ddworken, @punkeel, and @yoavweiss can help us think through that from their perspectives.
- Can we sign redirects generally? That seems straightforward, as we've described: sign
@statusandlocationin the same way we sign everything else, and deliver a network error rather than a redirect on failure. This seems like a reasonable capability to add in any event, and should be a relatively small extension to the existing profile.
+1, this seems quite reasonable and like something that could be easy to include in the MVP.
- Should we require redirects to be signed with a matching key if a client requires a public key match? This is certainly possible, and would fairly robustly (in combination with signing
@path) mitigate the risk that a compromised server hands out a response other than the one expected. We could easily implement this. The questions I have here is entirely practical: could deployments support this requirement, and should it be optional? Hopefully folks like @ddworken, @punkeel, and @yoavweiss can help us think through that from their perspectives.
This requirement seems reasonable and should be doable as long as we continue to send Accept-Signature on those requests.
Though from a security POV, I'm wondering: Is it actually necessary to sign all the redirect hops? As I see it, Signature SRI aims to ensure that the initial response to a JS request isn't forged. If that initial response triggers loading of other JS (either via eval, script#src, or a redirect) it seems like it could be reasonable to say that is out-of-scope for Signature SRI. And that if you don't want the redirect to lead to loading other JS, you shouldn't sign the initial first hop of the redirect chain. I'm not entirely sure if this argument makes sense, but I'd be curious for other's thoughts on this. :)
When discussing this with @martinthomson I suggested that resources be able to include a "name" in the signature that's distinct from the URL path. He pointed out that this idea is similar to the nonce discussed in #41, but I think it's important that the name not have to be used just once. The idea would be that you could sign multiple versions of the same resource with the same name as long as it's not insecure to substitute one for the other, and you could serve copies of that signed resource from lots of different places without breaking the signature. You need a similar way to attach the name to the integrity as in #41, but the integrity="ed25519-[key goes here]?name=[script-name]" syntax there seems to work for this too.
I've thought about this a bit on and off over the last few days, and my current take is that it's reasonable to be strict about the provenance checks that a page is asking for. Defining a variant of the profile that applies to redirects (as discussed above) seems like a straightforward solution, and hopefully won't complicate deployments too much.
@ddworken: I think it would be necessary to sign all the hops for the integrity check to be meaningful. I tried to spell out some of the things I'm worried about in https://wicg.github.io/signature-based-sri/#security-substitution, but at core we've modeled SRI as a network-level integrity check that aims to ensure that developers get the resource they're asking for, even in the presence of an attacker who controls the server from which they're receiving a script. From that perspective, I see redirect responses as being different in kind from eval() or import or etc.
@jyasskin: At the moment, I'd suggest that the URL a developer specifies is a reasonable-enough "name". If we require redirect responses to be signed, and encourage (require?) developers to sign @path/@query-param in their responses as well, I think we'd have a fairly robust system. I worry about the deployment cost of requiring those components, but I might be overestimating the difficulty of teaching a build system about resources' expected locations.
@ddworken: I think it would be necessary to sign all the hops for the integrity check to be meaningful. I tried to spell out some of the things I'm worried about in https://wicg.github.io/signature-based-sri/#security-substitution, but at core we've modeled SRI as a network-level integrity check that aims to ensure that developers get the resource they're asking for, even in the presence of an attacker who controls the server from which they're receiving a script. From that perspective, I see redirect responses as being different in kind from
eval()orimportor etc.
This seems reasonable to me, it is true that if someone chooses to sign a redirect and we didn't apply Signature SRI to post-redirect requests, that would essentially negate the benefits of Signature SRI. In my opinion an easy answer is just to not sign redirects, but I think checking the whole redirect chain is also quite reasonable. I don't anticipate this causing any deployment issues.
Shifting this to the future: https://wicg.github.io/signature-based-sri/#security-redirection spells out the current approach and the options we might want to explore later on, but current deployment experience suggests both that key segregation is good enough for a first pass and that we have reasonably deployable options if we decide we need them.
These suggestions for solving redirects, content-replacement across paths are all optional right now. Given that these are known possible attacks, it seems useful to solve them in the standard, rather than leaving them out.
I'm not sure what you mean by "solve them in the standard". The developers I've talked to about this have fairly universally been uninterested in locking down redirects, preferring instead to split resources by key. That said, I agree that defining the mechanism for redirects is probably worth doing as more developers get interested in the problem. I'm not sure requiring checks on redirects is the right thing to do, but I'm entirely on board with giving developers the ability to require such enforcement.
One potential approach would have two parts:
-
We'd define a signature profile for redirects with qualities that more or less matched the
ed25519-integritydefinition, but that replaced the requirement to include"unencoded-digest";sfwith a requirement to include"location"with no parameters. Likewise, we'd adjust the fetch integration to have this new profile checked when handling redirect responses in Fetch. -
We'd give developers a way of specifying whether a specified key was intended to guard redirects alone or in addition to the final response (perhaps as a parameter on the key, like
integrity="ed25519-...?verify=redirects ed25519-...?verify=all").
That seems like the minimal set of things we could do to create technical enforcement of redirect validity. Is it along the lines of what you're thinking, or are there other approaches we should consider?
I'm not sure why we couldn't require HTTP 30x responses to be signed at all times.
We can require anything we like, so long as it's shippable. My recollection is that developers deploying signatures didn't strongly object to requiring signed redirects, but also saw it as unnecessary complexity (for example because the signing keys are held by one team's build infrastructure that signs packages deployed in one place, and another team is responsible for the frontend architecture that produces redirects; sharing keys is hard, and allowing multiple keys makes an analysis of the actual security properties difficult).
Can someone explain the attack angle with a bit more words or a reference? mt talks about "resource swapping" above. Is the idea that the attacker holds the key to sign so they can sign arbitrary resources, but because they don't control the origins involved the attack fails? But if they could control a redirect, they would be able to attack?
(And none of this is really a problem with normal integrity because collisions are hard.)
@annevk: I hope that https://wicg.github.io/signature-based-sri/#security-substitution provides some context for the risks we're discussing. In short, an attacker who controls a server's responses (but does not possess the signing key) can redirect a response to any other resource: this is unfortunate if you've signed both good.js and bad.js with the same key.
With @path referring to the final hop, wouldn't you be protected against that still? How can redirects thwart that? If good and bad have the same path, but different authorities, I can see it, but it appears you could sign that as well if you are so inclined.
(I suppose part of the problem here is that these features are optional and so the actual integrity can vary quite a bit.)
With
@pathreferring to the final hop, wouldn't you be protected against that still? How can redirects thwart that?
Signing @path;req ensures that an attacker could not replace good.js with bad.js directly (which is valuable!). But attackers who control the server could instead return a redirect response for the request to /good.js with a location pointing to /bad.js. That response would be correctly signed against the request path of /bad.js.
Requiring the /good.js -> /bad.js redirect itself to be signed would require the attacker to possess the key, at which point no protection is possible.
(I suppose part of the problem here is that these features are optional and so the actual integrity can vary quite a bit.)
It might well be reasonable for us to increase the set of requirements to enforce a higher baseline, but it's not clear to me at the moment whether the cost of doing so is worthwhile. I can certainly imagine it being possible for build systems that hold keys to know the path at which a given resource is available, but I can also imagine developers who localize their paths being frustrated about needing to copy/resign resources for every supported language (see https://www.apple.com/ac/globalfooter/8/de_DE/scripts/ac-globalfooter.built.js and https://www.apple.com/ac/globalfooter/8/en_US/scripts/ac-globalfooter.built.js for example; I wouldn't be surprised if those mapped to the same file on the frontend). Signing @authority seems even harder to deal with, given staging and etc.
Ah, that makes sense. Now I understand @jyasskin's suggestion better, but not your response to him. You still seem to think @path is workable despite it being the request current URL path and not the request URL path?
Signing @path;req ensures that the response you're getting is a response the server intends to deliver for the current request. Signing redirects ensures that when the server changes the request target that it's equally intentional. Requiring both would, I think, make the system robust against an attacker who controls the frontend.
I worry a bit about the deployment cost of doing so, and it seems reasonable to me to treat them as optional for a first pass while we figure out what deployment challenges actually look like.
Well, Jeffrey's suggestion would side-step the need for redirects to be signed as the page and final resource agree on a stable identifier that's also signed. Deployment-wise that seems much more tractable?
Well, Jeffrey's suggestion would side-step the need for redirects to be signed as the page and final resource agree on a stable identifier that's also signed. Deployment-wise that seems much more tractable?
It's a totally reasonable suggestion. It seemed to me that having multiple ways to name a resource would couple frontend and backend changes even more tightly than key distribution already requires, but I can see how folks might come to the same conclusion about signed redirects.
As a data point, Google's experiments with this system have sidestepped the problem by minting different keys for resources of different types, which has the ~same effect as introducing the additional metadata that name might represent.
Google's experiments with this system have sidestepped the problem by minting different keys for resources of different types, which has the ~same effect as introducing the additional metadata that name might represent.
I don't think that addresses the concern. The attacks you described involve substitution of resources of the same type. Signing @path and redirects prevents that.
Note that these changes cannot protect against the use of the same signing key for different versions of the same resource. An old and compromised version of a script could be served in place of a newer one if you don't also invalidate (not just rotate) keys when a compromise is detected.
Worse, some conditions that might lead to a vulnerability aren't always identified as needing key invalidation. Combining v3 of a.js with v4 of b.js, because they were never conceived of as being combined in that way. That can lead to exploitable conditions. Consider that v3 of b.js originally hosted a security-critical check, but refactoring moved it to a.js in v4. Moving stuff around is perfectly normal, and it doesn't even need to be an action by a programmer, it might even happen automatically in a bundling layer. But it could lead to a disastrous outcome under this model.
Jeffrey's name idea doesn't address that problem either. Invalidating keys on each version rotation does, but that's probably operationally infeasible. Code changes constantly, but you can't respond to that by rotating keys for every change, even if you probably should.
I don't think that addresses the concern. The attacks you described involve substitution of resources of the same type.
Indeed. The robustness of key segregation depends entirely on the granularity at which keys are minted; at the limit, they're equivalent to name,
Signing @path and redirects prevents that. ... Note that these changes cannot protect against the use of the same signing key for different versions of the same resource.
I think we generally agree that requiring signatures over @path and on redirect would address the threats described in https://wicg.github.io/signature-based-sri/#security-replacement and https://wicg.github.io/signature-based-sri/#security-redirection by making it quite difficult for an attacker to substitute a signed resource at a given URL for another.
I would also agree that nothing we've discussed above would address rollback attacks (https://wicg.github.io/signature-based-sri/#security-rollback) which either reintroduce fixed bugs or create the unexpected combinations you mention. Key invalidation is one approach we could explore, but it introduces a ton of complexity that I'd honestly love to avoid. Relying on signature expiration is less robust, but could meaningfully limit an attacker's window of opportunity.
Jeffrey's name idea doesn't address [key invalidation] either. Invalidating keys on each version rotation does, but that's probably operationally infeasible. Code changes constantly, but you can't respond to that by rotating keys for every change, even if you probably should.
I agree. We're talking about a few different kinds of risks, and I think we'll require different mechanisms that we've discussed above for each. The core question for me isn't so much about which mitigations we'd recommend, but which we'd require. I'm quite willing to believe the initial pass we've implemented aims too low, but I'm wary of increasing deployment costs.
Feedback from developers would be helpful here.