Fossil User Forum

Etag If-None-Match bug?
Login

Etag If-None-Match bug?

Etag If-None-Match bug?

(1) By Joel Dueck (joeld) on 2025-05-25 15:48:45 [link] [source]

I think I’ve found a bug in etag.c, specifically the code that checks the If-None-Match header.

The value supplied for If-None-Match is supposed to be supplied inside double quotes (1, 2). But if I supply the value in this way, Fossil always returns 200 OK:

❯ curl -I https://joeldueck.com/code/pluto/pluto3.zip
HTTP/1.1 200 OK
Date: Sun, 25 May 2025 15:20:20 GMT
Server: Apache/2.4.41 (Ubuntu)
Cache-Control: max-age=0
X-Frame-Options: SAMEORIGIN
ETag: 91fa4ffb78dc18472435019085cd1ace
Content-Type: application/zip

❯ curl -I -H 'If-None-Match: "91fa4ffb78dc18472435019085cd1ace"' https://joeldueck.com/code/pluto/pluto3.zip
HTTP/1.1 200 OK
Date: Sun, 25 May 2025 15:21:01 GMT
Server: Apache/2.4.41 (Ubuntu)
Cache-Control: max-age=0
X-Frame-Options: SAMEORIGIN
ETag: 91fa4ffb78dc18472435019085cd1ace
Content-Type: application/zip

The current code in etag.c appears to compare the complete value supplied for If-None-Match without stripping off the double quotes. And indeed, if I supply the value without quotes (in violation of the spec) then Fossil returns the expected 304:

❯ curl -I --header 'If-None-Match: 91fa4ffb78dc18472435019085cd1ace' https://joeldueck.com/code/pluto/pluto3.zip
HTTP/1.1 304 Not Modified
Date: Sun, 25 May 2025 15:24:49 GMT
Server: Apache/2.4.41 (Ubuntu)
Cache-control: no-cache

Since these lines in etag.c have been in service for a good seven years now, there might be clients out there depending on the current behavior, but can we patch this so that quotes are detected and stripped out before the comparison?

Something like this (warning my C is very rusty):

  size_t nLen;
  /* ... */

  /* Check to see if the generated ETag matches If-None-Match and
  ** generate a 304 reply if it does. */
  zIfNoneMatch = P("HTTP_IF_NONE_MATCH");
  if( zIfNoneMatch==0 ) return;
  
  nLen = strlen(zIfNoneMatch);
  if( nLen >= 2 && zIfNoneMatch[0] == '"' && zIfNoneMatch[nLen-1] == '"' && memcmp(zIfNoneMatch+1, zETag, nLen-2) == 0) return;
  /* Retain old behavior in case there are clients relying on it. */
  if( strcmp(zIfNoneMatch,zETag)!=0 ) return;

(2) By Florian Balmer (florian.balmer) on 2025-05-25 16:48:00 in reply to 1 [link] [source]

Nice catch! And the fix doesn't look rusty at all!

Boring anecdote:

Several years ago, I was looking into problems with the If-Modified-Since headers, and also played around a bit with If-None-Match for comparison.

Both If-Modified-Since and If-None-Match were introduced with HTTP/1.1, but the Fossil built-in web server always responds with HTTP/1.0, so the web browsers used during testing received a LastModifiedTime or eTag, but never sent them back with conditional requests. Until I finally found out!

I was told the internal web server can't be changed to HTTP/1.1 because it's lacking some features required to comply with the standard.

Being a daredevil (well, sometimes), in my patched version of Fossil, this line has been hacked for years, and things seemed to work. Browsers are probably very forgiving about non-standard servers.

(3) By Joel Dueck (joeld) on 2025-05-29 20:02:17 in reply to 2 [source]

Thanks for the commit, I just tested it and it works great!

I’ve now realized Fossil has a matching bug on the other side of things: it supplies the value for the Etag header without putting it inside double quotes, and the spec says it must be inside quotes (1, 2). This is an issue because the tooling I'm working with follows the spec and will not make use of invalid Etag values...which in turn means it will fall back to full downloads, taking up bandwidth and compute on my server.

Could we also patch this line in cgi.c to bring it in line with the spec?

blob_appendf(&hdr, "ETag: \"%s\"\r\n", etag_tag());

I was told the internal web server can't be changed to HTTP/1.1 because it's lacking some features required to comply with the standard.

Interesting, I'm curious about what those are...but since my Fossil is CGI behind Apache it seems everything gets upgraded to 1.1 anyways.

(4) By Florian Balmer (florian.balmer) on 2025-05-30 04:48:02 in reply to 3 [link] [source]

I think this is safe, and is also what the althttpd web server does here and here.

Interesting, I'm curious about what those are...

I brought up the topic several times, but right now I can only find this old thread, and the answer why Fossil can't use HTTP/1.1 isn't there.

... but since my Fossil is CGI behind Apache it seems everything gets upgraded to 1.1 anyways.

Yes, this hides a few of the quirks.

I'm using If-Modified-Since in my scripting, so I don't need to store the ETags, and my experience and previous patches were focused on date/time stamps.