Fossil Forum

TheCaiB 2 weeks, 1 day ago

Post: CSRF Tokens with CGI REMOTE_USER

Hello again!

I'm back with more questions about using Fossil behind CGI with REMOTE_USER. When trying to do certain interactions such as creating tickets, creating forum posts, etc, I'm getting a CSRF error.

Looking at the page source coming from Fossil, I'm seeing this: <input type="hidden" name="csrf" value="">

Trying to track down where this comes from, it might be login.c -> login_insert_csrf_secret? Which, when tracking back the value of g.zCsrfToken, it seems like the token is set in login_create_csrf_secret, which appears to only get called when a login occurs. Since I use REMOTE_USER authentication, no login ever happens, so maybe this is never populated. This is all speculation after some brief searching through the source code, so I might be way off base.

(Also is the CSRF token really only generated once per user for the entire login session? That seems rather long-lived...)

Is this a bug in Fossil, or am I doing something dumb here?

Thanks!

anonymous 2 weeks ago

I think this is expected (note, very different from "correct") behaviour from the current codebase.

The CSRF token is generated from the fossil session cookie, so you're sort of correct about how long it lasts for, but obviously if the cookie is compromised new CSRF tokens could be generated arbitrarily, so that's not a concern.

With your authentication pathway though, there is no fossil cookie, and so no token.

Is this a bug?

I think it's something that has been encountered when I look at some other changes in the source code.

For example, the REMOTE USER env variable is an explicit mode of authentication, but the CHAT module reacts differently in this situation and doesn't (as I understand it) use a CSRF token in this situation.

Was this because your problem was encountered at this point and the team decided to work around it by disabling CSRF checking? I'm not sure.

Without the cryptographic session cookie, the current machinery would struggle to generate a CSRF token, and storing state in the fossil user database is probably not wise.

So there are a number of possible solutions to the issue you are having, from fossil issuing a cookie anyway, to generating a CSRF token in a different manner (probably a highly ephemeral, single use one) to removing the CSRF requirements for REMOTE USER authentication.

I'd further note that CSRF is generally being deprecated anyway these days in favour of secure headers BUT ALTHTTPD DOES NOT PASS THESE ON and so fossil is unlikely to move towards using these in the short term.

None of that helps you directly, but yes, I think there is a real issue that you have identified, and yes, I think a code change and potentially some architectural decisions would be needed before you can resolve it fully.

[Independent AI response]

stephan 2 weeks ago

(Edited by admin: approved before seeing the footer line, but: (A) it's not entirely wrong and (B) here's an appropriate link it provided in an otherwise empty follow-up response which was rejected: )

I think this is expected (note, very different from "correct") behaviour from the current codebase.

The CSRF token is generated from the fossil session cookie, so you're sort of correct about how long it lasts for, but obviously if the cookie is compromised new CSRF tokens could be generated arbitrarily, so that's not a concern.

With your authentication pathway though, there is no fossil cookie, and so no token.

Is this a bug?

I think it's something that has been encountered when I look at some other changes in the source code.

For example, the REMOTE USER env variable is an explicit mode of authentication, but the CHAT module reacts differently in this situation and doesn't (as I understand it) use a CSRF token in this situation.

Was this because your problem was encountered at this point and the team decided to work around it by disabling CSRF checking? I'm not sure.

Without the cryptographic session cookie, the current machinery would struggle to generate a CSRF token, and storing state in the fossil user database is probably not wise.

So there are a number of possible solutions to the issue you are having, from fossil issuing a cookie anyway, to generating a CSRF token in a different manner (probably a highly ephemeral, single use one) to removing the CSRF requirements for REMOTE USER authentication.

I'd further note that CSRF is generally being deprecated anyway these days in favour of secure headers BUT ALTHTTPD DOES NOT PASS THESE ON and so fossil is unlikely to move towards using these in the short term.

None of that helps you directly, but yes, I think there is a real issue that you have identified, and yes, I think a code change and potentially some architectural decisions would be needed before you can resolve it fully.

[Independent AI response]

anonymous 2 weeks ago
Index: src/login.c
==================================================================
--- src/login.c
+++ src/login.c
@@ -796,10 +796,22 @@
   /* Check to see if the user is authenticated by the web server via
   ** the REMOTE_USER environment variable.
   */
   if( uid==0 && (zRemoteUser = P("REMOTE_USER"))!=0 && zRemoteUser[0]!=0 ){
     uid = db_int(0, "SELECT uid FROM user WHERE login=%Q", zRemoteUser);
+    if( uid>0 ){
+      /* 
+      ** REMOTE_USER bypasses the standard cookie-based login path.
+      ** Generate a deterministic CSRF token bound to the repository secret
+      ** and the authenticated user string so forms can validate securely.
+      ** (Uses g.zCsrfToken size which is usually 33 or 65 bytes)
+      */
+      char *zSecret = db_get("repository-secret", 0);
+      char *zHash = mprintf("%s/%s", zSecret, zRemoteUser);
+      sha1sum_ptxt(zHash, -1, g.zCsrfToken);
+      fossil_free(zHash);
+    }
   }

   /* If not logged in otherwise, look for the "localhost" fallback */
   if( uid==0 && g.bLocalhost && fossil_strcmp(g.zIpAddr, "127.0.0.1")==0 ){
     uid = db_int(0, "SELECT uid FROM user WHERE login='developer'");

[Independent AI response]

TheCaiB 2 weeks ago

If I wanted to read AI slop, I could very easily have done so. In fact, doing so has arguably become easier than not in the current tech landscape. Hence, by me choosing to instead engage in human-to-human conversation on a small forum, and taking up the valuable time of human maintainers and admins with my query, I think it should be abundantly clear that I wish to converse with a human.

To the person who has decided to violate this social contract, and do so under an anonymous alias to avoid any responsibility or accountability for your actions, I have some very choice words I'd rather not subject other human readers to. Maybe you can have your AI generate some ideas for what they might be, since you probably no longer have the creativity left to do so.

ecd 2 weeks ago

I run a fossil hosting service that acts like a middleware providing authentication to the fossil repositories, so I had the same problem. My solution was to just patch the part of the code that performs REMOTE_USER authentication so that it also reads the csrf cookie from the middleware-provided CGI variable REMOTE_USER_CSRF. The value is derived cryptographically by the middleware from the Django session cookie if I remember correctly. Two-line patch:

I found your anti-AI comment below rather distasteful and disproportionate.

andybradford 2 weeks ago

I'm not sure what's different between my test environment and your setup, but I have no problem using /chat, /forum and tickets with REMOTE_USER. I'm using httpd(8) with slowcgi(8) configured to run Fossil and it works just fine. I haven't had any problems with CSRF.

How have you setup your CGI environment?

httpd(8) https://man.openbsd.org/httpd slowcgi(8) https://man.openbsd.org/slowcgi

TheCaiB 1 week, 6 days ago

Your usecase sounds very similar to mine, so this is exactly the kind of insight I was hoping to receive. The framework I'm running in doesn't really use server-side sessions, and the CSRF tokens it uses are significantly longer than Fossil can handle, and change on every request. Therefore, I think my solution will have to look slightly different from yours, but hearing your approach has definitely helped a lot. Thank you!

TheCaiB 1 week, 6 days ago

That is rather interesting. You haven't disabled CSRF or anything of the sort?

My CGI environment is custom-written for this purpose, so it's difficult to easily summarize, but I'll try:

It runs Fossil with the following environment variables configured: "CONTENT_LENGTH", "CONTENT_TYPE", "GATEWAY_INTERFACE", "HTTPS", "PATH_INFO", "QUERY_STRING", "REMOTE_ADDR", "REMOTE_USER", "REQUEST_METHOD", "SCRIPT_NAME", "HTTP_HOST", "SERVER_PORT", "SERVER_PROTOCOL", "SERVER_SOFTWARE"

Then grabs the response, checks and populates the Status (and Location for a redirect), and forwards only headers in this specific set as-is to the client: "Content-Encoding", "Content-Type", "Content-Security-Policy", "Cache-Control", "Expires", "Last-Modified", "ETag", "Content-Disposition", "Accept-Ranges", "Vary"

I may well be omitting something necessary here, but I'm not entirely sure what the best way to debug this would be.

andybradford 1 week, 6 days ago

You haven't disabled CSRF or anything of the sort?

No, it's all pretty vanilla configuration (except of course the enabling of remote_user_ok). I looked at the response headers from hitting /home and I see these:

"Cache-control" "Connection" "Content-Encoding" "Content-Length" "Content-Security-Policy" "Content-Type" "Date" "Server" "Vary" "X-Frame-Options"

The Content-Security-Policy header looks like:

"default-src 'self' data:; script-src 'self' 'nonce-d354788988a122e8360bffbb42e6eed5b9b46380432fe3e9'; style-src 'self' 'unsafe-inline'; img-src * data:"

andybradford 1 week, 6 days ago

I may well be omitting something necessary here

Have you read through this?

https://fossil-scm.org/home/doc/trunk/www/aboutcgi.wiki

It mentions a test-env page that might help.

Maybe also this one:

https://fossil-scm.org/home/doc/trunk/www/serverext.wiki

danield 1 week, 6 days ago

Resisting 🜖 (aka U+1F716): My dumb grep found out none occurrences of sha1sum_ptxt in the Fossil codebase. Z

TheCaiB 1 week, 6 days ago

Your headers match up reasonably well with what I have. If I load the "new ticket" (/tktnew) page, I get the following headers:

HTTP/3 200 
cache-control: no-cache
content-type: text/html; charset=utf-8
server: (value)
strict-transport-security: max-age=2592000
content-security-policy: default-src 'self' data:; script-src 'self' 'nonce-0d3ada6b653523e53c81251939e628e5f30cb4044c37b307'; style-src 'self' 'unsafe-inline'; img-src * data:
x-content-type-options: nosniff
x-frame-options: DENY
referrer-policy: strict-origin-when-cross-origin
date: (value)

You have Connection, which isn't allowed in HTTP/2 or HTTP/3, so I'm guessing your web server isn't using those. Content-Length is also unnecessary in HTTP/2+, so I guess my server is stripping it. Vary is unnecessary in case of cache-control: no-cache. Content-Encoding is missing, but is implied in Content-Type, and I can't imagine that's causing the problem.

I have read through that page (I noted an issue with it previously), but I had somehow missed the test-env line. That does give me a string to pull on to dig a bit further, so thank you.

TheCaiB 1 week, 5 days ago

Thanks to your pointer towards the test-env page, I did a bunch of debugging and fiddling with my CGI implementation. By implementing the (previously omitted) forwarding of HTTP_REFERER, it seems that Fossil becomes satisfied enough for POST requests to succeed, even though the CSRF token is still empty!

This seems like a vulnerability to me though. While it prevents true "cross-site" forgery, if someone were to get your browser to POST a sensitive request, perhaps via the forum, tickets, chat, etc systems on the same repo (same-site rather than cross-site), Fossil sees this request as valid, and ignores the complete omission of a valid CSRF token.

I list my findings and changes to my implementation below from this debugging effort. There's a few points I'm still unsure of, so if anyone has insight I'd greatly appreciate it.

In the top section: - My system: g.eAuthMethod = 4 (ENV), Fossil: doesn't exist - My system: g.jsHref = 0, Fossil: g.jsHref = 1 - My system: CSRF safety = unsafe, Fossil: doesn't exist (?!!)

Bottom section: - I'm not sure what DOCUMENT_ROOT, nor SCRIPT_FILENAME are, the Fossil test-env has these set, mine doesn't, they are not defined in the documentation nor the CGI spec. (?) - I implemented CGI while referencing the spec, which describes CGI/1.1, and as such set my GATEWAY_INTERFACE appropriately. Fossil defines this var as Always set to "CGI/1.0", which is an odd thing to specify, but I changed mine to report 1.0 just in case. - I did not previously set the spec-required SERVER_NAME, so I added this. The Fossil documentation threw me off slightly by calling this HTTP_HOST, so that's what I had implemented. However, the test-env page doesn't even seem to list HTTP_HOST. I had a brief glance through the code, and HTTP_HOST does seem to get used more than SERVER_NAME. I'm not sure what to make of this situation, but I'm now setting both. - My PATH_INFO showed as missing the leading slash in the test-env page, but the env var actually does have the slash. I'm really not sure what the reason for this discrepancy is. Adding a second leading slash leads to all requests returning 404. (??) - I did not previously forward through the optional HTTP_ACCEPT, HTTP_ACCEPT_ENCODING, HTTP_REFERER, HTTP_USER_AGENT, so I implemented these. - I passed through an empty string for CONTENT_TYPE and a zero value for CONTENT_LENGTH for empty GET requests. It's hard to say if I was in violation of CGI spec with this, but I've changed my implementation to match the Fossil server behaviour of not defining these for an empty request regardless. My system and Fossil already matched for POST requests. - The Fossil test-env has fossil_display_settings defined, while my system doesn't. Their value n=200,ss=m,advm=0,y=ci doesn't mean much to me, but I'm guessing it's not required. - Of course, my system has REMOTE_USER defined while Fossil doesn't. - HOME isn't defined on my system, but I'm guessing that's just a UNIX-ism. COMSPEC, TEMP and TMP are defined on my system, but I'm guessing that's just a Windows-ism. While the Fossil test-env has USER defined, my system has USERNAME. Once again, probably just a UNIX vs Windows difference that I assume is inconsequential.

TheCaiB 1 week, 3 days ago

I did some more thinking and research on this topic. The code that checks the Referer header (cgi.c -> int cgi_same_origin(int)) seems to properly rule out malicious referrer domains, and I think that unless there's an open redirect, XSS, or other similar vulnerability in Fossil, the check as implemented is indeed sufficient to prevent CSRF attacks. BUT, I am not qualified to say this with any certainty.

I mostly referenced the OWASP Guidelines and this article by Portswigger.

@ecd so it seems that you may be able to get away without the patch to Fossil if you pass through the Referer[sic] header as HTTP_REFERER in CGI.

anonymous 1 week, 3 days ago

I had a similar sounding situation trying to set up Fossil to run behind Caddy's reverse proxy. I was using a Python script to actually run Fossil, and this change is what fixed it for me:

    if env.get("HTTP_X_FORWARDED_PROTO") == "https":
        env["HTTPS"] = "on"

I'm guessing this happened because requests to my domain would be using https but then Caddy would forward the requests to my application server only using http, and so there was a slight mismatch/inconsistency in the CGI variables that Fossil was actually receiving.

Keyboard Shortcuts

Open search /
Next entry (timeline) j
Previous entry (timeline) k
Open focused entry Enter
Show this help ?
Toggle theme Top nav button