Skip to content

Commit cf59027

Browse files
authored
Merge pull request #1379 from haskell/user-content-host-v2
User content host v2
2 parents 1bb90f4 + a454a13 commit cf59027

File tree

10 files changed

+172
-47
lines changed

10 files changed

+172
-47
lines changed

exes/Main.hs

Lines changed: 28 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -197,6 +197,8 @@ data RunFlags = RunFlags {
197197
flagRunPort :: Flag String,
198198
flagRunIP :: Flag String,
199199
flagRunHostURI :: Flag String,
200+
flagRunUserContentURI :: Flag String,
201+
flagRunRequiredBaseHostHeader :: Flag String,
200202
flagRunStateDir :: Flag FilePath,
201203
flagRunStaticDir :: Flag FilePath,
202204
flagRunTmpDir :: Flag FilePath,
@@ -215,6 +217,8 @@ defaultRunFlags = RunFlags {
215217
flagRunPort = NoFlag,
216218
flagRunIP = NoFlag,
217219
flagRunHostURI = NoFlag,
220+
flagRunUserContentURI = NoFlag,
221+
flagRunRequiredBaseHostHeader = NoFlag,
218222
flagRunStateDir = NoFlag,
219223
flagRunStaticDir = NoFlag,
220224
flagRunTmpDir = NoFlag,
@@ -264,6 +268,14 @@ runCommand =
264268
"Server's public base URI (defaults to machine name)"
265269
flagRunHostURI (\v flags -> flags { flagRunHostURI = v })
266270
(reqArgFlag "NAME")
271+
, option [] ["user-content-uri"]
272+
"Server's public user content base URI (for untrusted content, defeating XSS style attacks)"
273+
flagRunUserContentURI (\v flags -> flags { flagRunUserContentURI = v })
274+
(reqArgFlag "NAME")
275+
, option [] ["required-base-host-header"]
276+
"Required host header value for incoming requests (potentially internal, e.g. if behind reverse proxy). Base means that it is _not_ for the user-content domain."
277+
flagRunRequiredBaseHostHeader (\v flags -> flags { flagRunRequiredBaseHostHeader = v })
278+
(reqArgFlag "NAME")
267279
, optionStateDir
268280
flagRunStateDir (\v flags -> flags { flagRunStateDir = v })
269281
, optionStaticDir
@@ -307,6 +319,8 @@ runAction opts = do
307319
port <- checkPortOpt defaults (flagToMaybe (flagRunPort opts))
308320
ip <- checkIPOpt defaults (flagToMaybe (flagRunIP opts))
309321
hosturi <- checkHostURI defaults (flagToMaybe (flagRunHostURI opts)) port
322+
usercontenturi <- checkUserContentURI defaults (flagToMaybe (flagRunUserContentURI opts))
323+
requiredbasehostheader <- checkRequiredBaseHostHeader defaults (flagToMaybe (flagRunRequiredBaseHostHeader opts))
310324
cacheDelay <- checkCacheDelay defaults (flagToMaybe (flagRunCacheDelay opts))
311325
let stateDir = fromFlagOrDefault (confStateDir defaults) (flagRunStateDir opts)
312326
staticDir = fromFlagOrDefault (confStaticDir defaults) (flagRunStaticDir opts)
@@ -317,6 +331,8 @@ runAction opts = do
317331
}
318332
config = defaults {
319333
confHostUri = hosturi,
334+
confUserContentUri = usercontenturi,
335+
confRequiredBaseHostHeader = requiredbasehostheader,
320336
confListenOn = listenOn,
321337
confStateDir = stateDir,
322338
confStaticDir = staticDir,
@@ -380,20 +396,28 @@ runAction opts = do
380396
++ "\n(you can override with the --base-uri= flag)"
381397
return guessURI'
382398

383-
checkHostURI _ (Just str) _ = case parseAbsoluteURI str of
399+
checkHostURI _ (Just str) _ = validateURI str
400+
401+
validateURI str = case parseAbsoluteURI str of
384402
Nothing -> fail $ "Cannot parse as a URI: " ++ str ++ "\n"
385403
++ "Make sure you include the http:// part"
386404
Just uri
387405
| uriScheme uri `notElem` ["http:", "https:"] ->
388406
fail $ "Sorry, the server assumes it will be served (or proxied) "
389407
++ " via http or https, so cannot use uri scheme " ++ uriScheme uri
390408
| isNothing (uriAuthority uri) ->
391-
fail "The base-uri has to include the full host name"
409+
fail "The base-uri and the user-content-uri have to include full host names"
392410
| uriPath uri `notElem` ["", "/"] ->
393-
fail $ "Sorry, the server assumes the base-uri to be at the root of "
394-
++ " the domain, so cannot use " ++ uriPath uri
411+
fail $ "Sorry, the server assumes base-uri and user-content-uri to be at the root of "
412+
++ " their domains, so cannot use " ++ uriPath uri
395413
| otherwise -> return uri { uriPath = "" }
396414

415+
checkUserContentURI _ Nothing = fail "You must provide the --user-content-uri= flag"
416+
checkUserContentURI _ (Just str) = validateURI str
417+
418+
checkRequiredBaseHostHeader _ Nothing = fail "You must provide the --required-base-host-header= flag. It's typically the host part of the base-uri."
419+
checkRequiredBaseHostHeader _ (Just str) = pure str
420+
397421
checkIPOpt defaults Nothing = return (loIP (confListenOn defaults))
398422
checkIPOpt _ (Just str) =
399423
let pQuad = do ds <- Parse.many1 Parse.digit

src/Distribution/Server.hs

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,8 @@ data ListenOn = ListenOn {
6868
data ServerConfig = ServerConfig {
6969
confVerbosity :: Verbosity,
7070
confHostUri :: URI,
71+
confUserContentUri :: URI,
72+
confRequiredBaseHostHeader :: String,
7173
confListenOn :: ListenOn,
7274
confStateDir :: FilePath,
7375
confStaticDir :: FilePath,
@@ -96,6 +98,8 @@ defaultServerConfig = do
9698
uriScheme = "http:",
9799
uriAuthority = Just (URIAuth "" hostName (':' : show portnum))
98100
},
101+
confUserContentUri = nullURI, -- This is a required argument, so the default doesn't matter
102+
confRequiredBaseHostHeader = "", -- This is a required argument, so the default doesn't matter
99103
confListenOn = ListenOn {
100104
loPortNum = 8080,
101105
loIP = "127.0.0.1"
@@ -122,7 +126,7 @@ hasSavedState :: ServerConfig -> IO Bool
122126
hasSavedState = doesDirectoryExist . confDbStateDir
123127

124128
mkServerEnv :: ServerConfig -> IO ServerEnv
125-
mkServerEnv config@(ServerConfig verbosity hostURI _
129+
mkServerEnv config@(ServerConfig verbosity hostURI userContentURI requiredBaseHostHeader _
126130
stateDir _ tmpDir
127131
cacheDelay liveTemplates) = do
128132
createDirectoryIfMissing False stateDir
@@ -147,6 +151,8 @@ mkServerEnv config@(ServerConfig verbosity hostURI _
147151
serverTmpDir = tmpDir,
148152
serverCacheDelay = cacheDelay * 1000000, --microseconds
149153
serverBaseURI = hostURI,
154+
serverUserContentBaseURI = userContentURI,
155+
serverRequiredBaseHostHeader = requiredBaseHostHeader,
150156
serverVerbosity = verbosity
151157
}
152158
return env

src/Distribution/Server/Features/Documentation.hs

Lines changed: 25 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ module Distribution.Server.Features.Documentation (
77
initDocumentationFeature
88
) where
99

10+
import Distribution.Server.Features.Security.SHA256 (sha256)
1011
import Distribution.Server.Framework
1112

1213
import Distribution.Server.Features.Documentation.State
@@ -43,7 +44,8 @@ import Data.Function (fix)
4344

4445
import Data.Aeson (toJSON)
4546
import Data.Maybe
46-
import Data.Time.Clock (NominalDiffTime, diffUTCTime, getCurrentTime)
47+
import Data.Time.Calendar (fromGregorian)
48+
import Data.Time.Clock (NominalDiffTime, UTCTime(..), diffUTCTime, getCurrentTime)
4749
import System.Directory (getModificationTime)
4850
import Control.Applicative
4951
import Distribution.Server.Features.PreferredVersions
@@ -154,7 +156,7 @@ documentationFeature :: String
154156
-> Hook PackageId ()
155157
-> DocumentationFeature
156158
documentationFeature name
157-
ServerEnv{serverBlobStore = store, serverBaseURI}
159+
env@ServerEnv{serverBlobStore = store, serverBaseURI}
158160
CoreResource{
159161
packageInPath
160162
, guardValidPackageId
@@ -291,11 +293,29 @@ documentationFeature name
291293
etag = BlobStorage.blobETag blob
292294
-- if given a directory, the default page is index.html
293295
-- the root directory within the tarball is e.g. foo-1.0-docs/
296+
mtime <- liftIO $ getModificationTime tarball
294297
age <- liftIO $ getFileAge tarball
295298
let maxAge = documentationCacheTime age
296-
ServerTarball.serveTarball (display pkgid ++ " documentation")
297-
[{-no index-}] (display pkgid ++ "-docs")
298-
tarball index [Public, maxAge] etag (Just rewriteDocs)
299+
tarServe <-
300+
ServerTarball.serveTarball (display pkgid ++ " documentation")
301+
[{-no index-}] (display pkgid ++ "-docs")
302+
tarball index [Public, maxAge] etag (Just rewriteDocs)
303+
case tarServe of
304+
ServerTarball.TarDir response -> pure response
305+
ServerTarball.TarFile fileContent response -> do
306+
let
307+
digest = show $ sha256 fileContent
308+
-- Because JSON files cannot execute code or affect layout, we don't need to verify anything else
309+
isDocIndex =
310+
case dpath of
311+
("..","doc-index.json") : _ -> True
312+
_ -> False
313+
if mtime < UTCTime (fromGregorian 2025 2 1) 0
314+
|| isDocIndex
315+
|| digest == "548d676b3e5a52cbfef06d7424ec065c1f34c230407f9f5dc002c27a9666bec4" -- quick-jump.min.js
316+
|| digest == "6bd159f6d7b1cfef1bd190f1f5eadcd15d35c6c567330d7465c3c35d5195bc6f" -- quick-jump.css
317+
then pure response
318+
else requireUserContent env response
299319

300320
rewriteDocs :: BSL.ByteString -> BSL.ByteString
301321
rewriteDocs dochtml = case BSL.breakFindAfter (BS.pack "<head>") dochtml of

src/Distribution/Server/Features/PackageCandidates.hs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -609,7 +609,7 @@ candidatesFeature ServerEnv{serverBlobStore = store}
609609
Left err ->
610610
errNotFound "Could not serve package contents" [MText err]
611611
Right (fp, etag, index) ->
612-
serveTarball (display (packageId pkg) ++ " candidate source tarball")
612+
tarServeResponse <$> serveTarball (display (packageId pkg) ++ " candidate source tarball")
613613
["index.html"] (display (packageId pkg)) fp index
614614
[Public, maxAgeMinutes 5] etag Nothing
615615

src/Distribution/Server/Features/PackageContents.hs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -206,7 +206,7 @@ packageContentsFeature CoreFeature{ coreResource = CoreResource{
206206
Left err ->
207207
errNotFound "Could not serve package contents" [MText err]
208208
Right (fp, etag, index) ->
209-
serveTarball (display (packageId pkg) ++ " source tarball")
209+
tarServeResponse <$> serveTarball (display (packageId pkg) ++ " source tarball")
210210
[] (display (packageId pkg)) fp index
211211
[Public, maxAgeDays 30] etag Nothing
212212

src/Distribution/Server/Features/UserNotify.hs

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -441,7 +441,7 @@ initUserNotifyFeature :: ServerEnv
441441
-> ReverseFeature
442442
-> VouchFeature
443443
-> IO UserNotifyFeature)
444-
initUserNotifyFeature env@ServerEnv{ serverStateDir, serverTemplatesDir,
444+
initUserNotifyFeature ServerEnv{ serverStateDir, serverTemplatesDir,
445445
serverTemplatesMode } = do
446446
-- Canonical state
447447
notifyState <- notifyStateComponent serverStateDir
@@ -452,7 +452,7 @@ initUserNotifyFeature env@ServerEnv{ serverStateDir, serverTemplatesDir,
452452
[ "user-notify-form.html", "endorsements-complete.txt" ]
453453

454454
return $ \users core uploadfeature adminlog userdetails reports tags revers vouch -> do
455-
let feature = userNotifyFeature env
455+
let feature = userNotifyFeature
456456
users core uploadfeature adminlog userdetails reports tags
457457
revers vouch notifyState templates
458458
return feature
@@ -576,8 +576,7 @@ pkgInfoToPkgId :: PkgInfo -> PackageIdentifier
576576
pkgInfoToPkgId pkgInfo =
577577
PackageIdentifier (packageName pkgInfo) (packageVersion pkgInfo)
578578

579-
userNotifyFeature :: ServerEnv
580-
-> UserFeature
579+
userNotifyFeature :: UserFeature
581580
-> CoreFeature
582581
-> UploadFeature
583582
-> AdminLogFeature
@@ -589,8 +588,7 @@ userNotifyFeature :: ServerEnv
589588
-> StateComponent AcidState NotifyData
590589
-> Templates
591590
-> UserNotifyFeature
592-
userNotifyFeature serverEnv@ServerEnv{serverCron}
593-
UserFeature{..}
591+
userNotifyFeature UserFeature{..}
594592
CoreFeature{..}
595593
UploadFeature{..}
596594
AdminLogFeature{..}
@@ -603,6 +601,7 @@ userNotifyFeature serverEnv@ServerEnv{serverCron}
603601
= UserNotifyFeature {..}
604602

605603
where
604+
ServerEnv {serverCron} = userFeatureServerEnv
606605
userNotifyFeatureInterface = (emptyHackageFeature "user-notify") {
607606
featureDesc = "Notifications to users on metadata updates."
608607
, featureResources = [userNotifyResource] -- TODO we can add json features here for updating prefs
@@ -717,7 +716,7 @@ userNotifyFeature serverEnv@ServerEnv{serverCron}
717716
vouchNotifications <- fmap (, NotifyVouchingCompleted) <$> drainQueuedNotifications
718717

719718
emails <-
720-
getNotificationEmails serverEnv userDetailsFeature users templates $
719+
getNotificationEmails userFeatureServerEnv userDetailsFeature users templates $
721720
concat
722721
[ revisionUploadNotifications
723722
, groupActionNotifications

src/Distribution/Server/Features/Users.hs

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -144,7 +144,8 @@ data UserFeature = UserFeature {
144144
-- | For a given user, return all of the URIs for groups they are in.
145145
getGroupIndex :: forall m. (Functor m, MonadIO m) => UserId -> m [String],
146146
-- | For a given URI, get a GroupDescription for it, if one can be found.
147-
getIndexDesc :: forall m. MonadIO m => String -> m GroupDescription
147+
getIndexDesc :: forall m. MonadIO m => String -> m GroupDescription,
148+
userFeatureServerEnv :: ServerEnv
148149
}
149150

150151
instance IsHackageFeature UserFeature where
@@ -227,7 +228,7 @@ deriveJSON (compatAesonOptionsDropPrefix "ui_") ''UserGroupResource
227228

228229
-- TODO: add renaming
229230
initUserFeature :: ServerEnv -> IO (IO UserFeature)
230-
initUserFeature ServerEnv{serverStateDir, serverTemplatesDir, serverTemplatesMode} = do
231+
initUserFeature serverEnv@ServerEnv{serverStateDir, serverTemplatesDir, serverTemplatesMode} = do
231232
-- Canonical state
232233
usersState <- usersStateComponent serverStateDir
233234
adminsState <- adminsStateComponent serverStateDir
@@ -261,6 +262,7 @@ initUserFeature ServerEnv{serverStateDir, serverTemplatesDir, serverTemplatesMod
261262
groupIndex
262263
userAdded authFailHook groupChangedHook
263264
adminG adminR
265+
serverEnv
264266

265267
(adminG, adminR) <- groupResourceAt "/users/admins/" adminGroupDesc
266268

@@ -301,10 +303,11 @@ userFeature :: Templates
301303
-> Hook (GroupDescription, Bool, UserId, UserId, String) ()
302304
-> UserGroup
303305
-> GroupResource
306+
-> ServerEnv
304307
-> (UserFeature, UserGroup)
305308
userFeature templates usersState adminsState
306309
groupIndex userAdded authFailHook groupChangedHook
307-
adminGroup adminResource
310+
adminGroup adminResource userFeatureServerEnv
308311
= (UserFeature {..}, adminGroupDesc)
309312
where
310313
userFeatureInterface = (emptyHackageFeature "users") {
@@ -484,7 +487,7 @@ userFeature templates usersState adminsState
484487
-- See note about "authn" cookie above
485488
guardAuthenticatedWithErrHook :: Users.Users -> ServerPartE UserId
486489
guardAuthenticatedWithErrHook users = do
487-
(uid,_) <- Auth.checkAuthenticated realm users
490+
(uid,_) <- Auth.checkAuthenticated realm users userFeatureServerEnv
488491
>>= either handleAuthError return
489492
addCookie Session (mkCookie "authn" "1")
490493
-- Set-Cookie:authn="1";Path=/;Version="1"
@@ -493,6 +496,8 @@ userFeature templates usersState adminsState
493496
realm = Auth.hackageRealm --TODO: should be configurable
494497

495498
handleAuthError :: Auth.AuthError -> ServerPartE a
499+
handleAuthError Auth.BadHost { actualHost, oughtToBeHost } =
500+
errForbidden "Bad Host" [MText $ "Authenticated resources can only be accessed using the regular server host name " <> oughtToBeHost <> ", but was provided host " <> show actualHost]
496501
handleAuthError err = do
497502
defaultResponse <- Auth.authErrorResponse realm err
498503
overrideResponse <- msum <$> runHook authFailHook err
@@ -513,7 +518,7 @@ userFeature templates usersState adminsState
513518
_ -> pure ()
514519

515520
users <- queryGetUserDb
516-
either (const Nothing) Just `fmap` Auth.checkAuthenticated Auth.hackageRealm users
521+
either (const Nothing) Just `fmap` Auth.checkAuthenticated Auth.hackageRealm users userFeatureServerEnv
517522

518523
-- | Resources representing the collection of known users.
519524
--

0 commit comments

Comments
 (0)