Drop cached dinner responses and keep link tokens out of names (PR #97 review F1, F2)
F1: bump the service worker cache version to v2 so activate deletes the v1 API cache, whose dinner responses still carried the pasted recipe_url and could be served offline. F2: nameFromURL now drops words holding a digit and ends the name at the first credential word (token, access, secret, ...), so a token in the link's last path segment never reaches the public dinner name. /recipes/beef-stew is still "Beef stew". Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -496,10 +496,19 @@ func checkRecipeURL(raw string) (*url.URL, error) {
|
||||
return u, nil
|
||||
}
|
||||
|
||||
// credentialWords end the name nameFromURL takes from a slug: a pasted link
|
||||
// can carry a personal token in its path, as in /stew-access_token-abc123,
|
||||
// and the name is shown in every dinner response.
|
||||
var credentialWords = map[string]bool{
|
||||
"access": true, "api": true, "apikey": true, "auth": true, "credential": true,
|
||||
"credentials": true, "passcode": true, "password": true, "passwd": true,
|
||||
"secret": true, "session": true, "sig": true, "signature": true, "token": true,
|
||||
}
|
||||
|
||||
// nameFromURL names a dinner from the last path segment of u: separators
|
||||
// become spaces, a file extension and words of only digits are dropped and
|
||||
// the first letter is capitalized, so
|
||||
// https://example.com/recipes/12345/chicken-tikka-masala/ is
|
||||
// become spaces, a file extension and words holding a digit are dropped, the
|
||||
// name ends at the first of credentialWords and the first letter is
|
||||
// capitalized, so https://example.com/recipes/12345/chicken-tikka-masala/ is
|
||||
// "Chicken tikka masala". ok is false when no usable name is left.
|
||||
func nameFromURL(u *url.URL) (name string, ok bool) {
|
||||
segments := strings.FieldsFunc(u.Path, func(r rune) bool { return r == '/' })
|
||||
@@ -512,8 +521,12 @@ func nameFromURL(u *url.URL) (name string, ok bool) {
|
||||
}
|
||||
var words []string
|
||||
for _, w := range strings.FieldsFunc(slug, func(r rune) bool { return r == '-' || r == '_' || r == '+' || unicode.IsSpace(r) }) {
|
||||
if strings.IndexFunc(w, func(r rune) bool { return !unicode.IsDigit(r) }) >= 0 {
|
||||
words = append(words, strings.ToLower(w))
|
||||
w = strings.ToLower(w)
|
||||
if credentialWords[w] {
|
||||
break
|
||||
}
|
||||
if strings.IndexFunc(w, unicode.IsDigit) < 0 {
|
||||
words = append(words, w)
|
||||
}
|
||||
}
|
||||
name = strings.Join(words, " ")
|
||||
|
||||
@@ -19,6 +19,13 @@ func TestNameFromURL(t *testing.T) {
|
||||
{"https://example.com/", ""},
|
||||
{"https://example.com/recipe/98765", ""},
|
||||
{"https://example.com/the-best-easy-recipe", ""},
|
||||
{"https://example.com/best-chili-v2", "Best chili"},
|
||||
{"https://example.com/stew-access_token-personaltokenabc123", "Stew"},
|
||||
{"https://example.com/stew-token-abcdefghijk", "Stew"},
|
||||
{"https://example.com/chili-api-key-zyxwvut", "Chili"},
|
||||
{"https://example.com/stew-x7k2p9q4", "Stew"},
|
||||
{"https://example.com/recipes/token-abcdefghijk", ""},
|
||||
{"https://example.com/key-lime-pie", "Key lime pie"},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
u, err := url.Parse(tt.link)
|
||||
|
||||
@@ -266,6 +266,36 @@ func TestDinnerResponsesHideRecipeURL(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// TestDinnerResponsesHideTokenInLinkPath: a personal token can sit in the
|
||||
// link's last path segment, which names the dinner, so the name stops before
|
||||
// it and neither the create response nor a later list read shows it.
|
||||
func TestDinnerResponsesHideTokenInLinkPath(t *testing.T) {
|
||||
t.Parallel()
|
||||
router, _ := newDinnersRouter(t)
|
||||
|
||||
const link = "https://c.example/recipes/stew-access_token-personaltokenabc123"
|
||||
rec, body := callDinners(t, router, http.MethodPost, dinnersPath, `{"recipe_url":"`+link+`"}`)
|
||||
if rec.Code != http.StatusCreated {
|
||||
t.Fatalf("status = %d, want 201: %s", rec.Code, rec.Body)
|
||||
}
|
||||
if body["name"] != "Stew" {
|
||||
t.Errorf("name = %v, want Stew", body["name"])
|
||||
}
|
||||
created := rec.Body.String()
|
||||
rec, _ = callDinners(t, router, http.MethodGet, dinnersPath+"?status=regular", "")
|
||||
if rec.Code != http.StatusOK {
|
||||
t.Fatalf("GET status=regular = %d: %s", rec.Code, rec.Body)
|
||||
}
|
||||
for _, resp := range []struct{ name, body string }{{"create", created}, {"list", rec.Body.String()}} {
|
||||
lower := strings.ToLower(resp.body)
|
||||
for _, secret := range []string{"personaltokenabc123", "abc123", "access", "token"} {
|
||||
if strings.Contains(lower, secret) {
|
||||
t.Errorf("%s response contains %q: %s", resp.name, secret, resp.body)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestCreateDinnerDuplicate(t *testing.T) {
|
||||
t.Parallel()
|
||||
router, st := newDinnersRouter(t)
|
||||
|
||||
Reference in New Issue
Block a user