Add security model and SECURITY.md

Document who Roller trusts, what each kind of user may do, the
boundaries Roller keeps, and what is out of scope. Add a SECURITY.md
that explains how to report issues and links the model.
diff --git a/SECURITY.md b/SECURITY.md
new file mode 100644
index 0000000..1657549
--- /dev/null
+++ b/SECURITY.md
@@ -0,0 +1,50 @@
+<!--
+Licensed to the Apache Software Foundation (ASF) under one or more
+contributor license agreements.  See the NOTICE file distributed with
+this work for additional information regarding copyright ownership.
+The ASF licenses this file to You under the Apache License, Version 2.0
+(the "License"); you may not use this file except in compliance with
+the License.  You may obtain a copy of the License at
+
+    https://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+-->
+
+# Security policy
+
+## Reporting a vulnerability
+
+Report suspected security vulnerabilities in Apache Roller privately to the
+Apache Security Team at [security@apache.org](mailto:security@apache.org).
+Please do **not** report them in the public issue tracker, in GitHub issues or
+pull requests, or on the public mailing lists.
+
+A useful report says which Roller version you tested, how the site is
+configured in any way that matters to the issue, and what steps reproduce the
+behaviour. If you are not sure whether what you found is a vulnerability,
+report it privately anyway and we will work it out with you.
+
+The Apache Security Team forwards the report to the Roller PMC. See the
+[ASF security page](https://www.apache.org/security/) for the foundation-wide
+process.
+
+## Security model
+
+The [Roller security model](docs/security-model.md) explains who Roller trusts,
+what each kind of user may do, and what counts as a vulnerability. Please read
+it before you report.
+
+## Supported versions
+
+Security fixes are made on the current release line and shipped in a new
+release. Older releases are not patched. If you run an older version, upgrade
+to the [current release](https://roller.apache.org/downloads/downloads.html).
+
+## Advisories
+
+Advisories are listed at <https://roller.apache.org/security.html>.
diff --git a/docs/README.md b/docs/README.md
index 8036aaf..6991392 100644
--- a/docs/README.md
+++ b/docs/README.md
@@ -5,6 +5,7 @@
 * `roller-install-guide.adoc` - How to install Roller
 * `roller-user-guide.adoc` - How to use Roller
 * `roller-template-guide.adoc` - How to create Roller templates and themes
+* `security-model.md` - Who Roller trusts, what each user may do, and what counts as a vulnerability
 * `examples/` - example configuration and script files
 * `testing/` - Various test scripts
 
diff --git a/docs/security-model.md b/docs/security-model.md
new file mode 100644
index 0000000..93ad08b
--- /dev/null
+++ b/docs/security-model.md
@@ -0,0 +1,360 @@
+<!--
+Licensed to the Apache Software Foundation (ASF) under one or more
+contributor license agreements.  See the NOTICE file distributed with
+this work for additional information regarding copyright ownership.
+The ASF licenses this file to You under the Apache License, Version 2.0
+(the "License"); you may not use this file except in compliance with
+the License.  You may obtain a copy of the License at
+
+    https://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+-->
+
+# Apache Roller security model
+
+## About this document
+
+- **Version**: this model describes the Apache Roller 6.1.x release line.
+- **Purpose**: it tells operators what Roller protects and what it leaves to them, and tells security researchers what counts as a vulnerability.
+- **Reporting**: report a suspected vulnerability privately to security@apache.org, as described in [SECURITY.md](../SECURITY.md).
+- **Vulnerabilities**: an issue is a vulnerability when an untrusted user crosses one of the boundaries below in a supported configuration.
+- **Questions**: ask about the model on the Roller dev list, but never post details of a suspected vulnerability there.
+
+## The one rule
+
+Roller is one server that hosts many weblogs for people who do not
+necessarily trust each other. The whole model follows from one rule:
+
+> **What you may do in one weblog gives you nothing in another weblog, and
+> nothing on the server.**
+
+Only two parties are trusted: the **global admin**, who runs the site, and the
+**operator**, who runs the server. Everyone else is a possible attacker. That
+includes the admin of a weblog.
+
+## Principles
+
+Every change to Roller must keep these rules:
+
+1. Rights in one weblog give nothing in another weblog or on the server.
+2. Weblog admins are untrusted. Only global admins and the operator are
+   trusted.
+3. Every object named in a request is checked against the weblog the action
+   runs on.
+4. Content that users write never runs as script for other users.
+5. Templates run in a sandbox. They cannot call arbitrary Java code or read
+   files outside their theme.
+6. Request data is data. Roller never runs it as code, and it never resolves
+   external entities in XML it receives.
+7. Every state-changing request from a logged-in user needs a one-time token
+   (the *salt*).
+8. Users below global admin cannot make the server fetch a URL of their
+   choice.
+
+## What Roller is made of
+
+```mermaid
+flowchart TB
+  subgraph Public["Public, no login"]
+    PR["Weblog pages, feeds, search"]
+    CM["Comments"]
+  end
+  subgraph Members["Logged-in users"]
+    AU["Authoring UI<br/>(/roller-ui/authoring)"]
+    API["Remote publishing APIs<br/>XML-RPC, AtomPub, OAuth"]
+  end
+  subgraph Admins["Global admins"]
+    AD["Admin UI<br/>(/roller-ui/admin)"]
+    PL["Planet feed aggregator"]
+    PG["Ping targets"]
+  end
+  subgraph Setup["First run only"]
+    IN["Installer + setup token"]
+  end
+  TH["Bundled themes and templates"]
+  DB[("Database, media files,<br/>search index")]
+  PR --> TH
+  PR --> DB
+  CM --> DB
+  AU --> DB
+  API --> DB
+  AD --> DB
+  IN --> DB
+  PL -->|fetches| NET(("Network"))
+  PG -->|pings| NET
+```
+
+Everything in the Roller WAR is in scope, including the bundled themes. Build
+and test tooling (`db-utils/`, `it-selenium/`, `testing/`, `docker/`,
+`assembly-release/`) is not part of the product and is out of scope. The
+Docker files are examples for development, not a supported deployment.
+
+## The users
+
+Roller has two layers of roles:
+
+- **Global roles** apply to the whole site.
+- **Weblog roles** apply to one weblog. A person can hold a different weblog
+  role in each weblog they belong to, or no role at all.
+
+```mermaid
+flowchart TB
+  subgraph Outside["Outside Roller"]
+    OP["Operator<br/>container, database, config, logs<br/>TRUSTED, out of scope"]
+  end
+  subgraph Site["Site-wide: global roles"]
+    GA["Global admin<br/>global role 'admin'<br/>TRUSTED"]
+    RU["Registered user<br/>global role 'editor'<br/>UNTRUSTED"]
+    AN["Anonymous visitor<br/>no account<br/>UNTRUSTED"]
+  end
+  subgraph Weblog["Inside one weblog: weblog roles"]
+    WA["Admin<br/>permission 'admin'<br/>UNTRUSTED"]
+    AU["Author<br/>permission 'post'"]
+    LI["Limited<br/>permission 'edit_draft'"]
+  end
+  OP -->|creates first admin at setup| GA
+  AN -->|registers, if allowed| RU
+  RU -->|creates a weblog| WA
+  RU -->|is invited to a weblog| AU
+  RU -->|is invited to a weblog| LI
+  GA -.->|holds every weblog role<br/>in every weblog| WA
+```
+
+Weblog roles stack: an Admin can do everything an Author can, and an Author can
+do everything a Limited member can.
+
+| User | Can do | Cannot do | Trusted? |
+| --- | --- | --- | --- |
+| **Anonymous visitor** | Read published entries, feeds and search; post comments; register, when registration is on | See drafts, settings or anything unpublished; change anything | No |
+| **Registered user** (global `editor`) | Log in; edit own profile; comment; create weblogs, when the site allows it | Act in a weblog where they hold no role | No |
+| **Limited** (`edit_draft`) | Create and edit draft entries in that weblog | Publish; manage comments, categories or media; act in other weblogs | No |
+| **Author** (`post`) | Everything Limited can do; publish entries; manage comments and categories; manage media, when uploads are on | Change weblog settings, members, bookmarks, pings or design | No |
+| **Admin** (`admin`) | Everything Author can do; weblog settings, members, bookmarks (including OPML import), pings, maintenance; theme and templates, when custom themes are on | Touch other weblogs; read server files; run Java code; read other users' private data; run script in other users' browsers | **No** |
+| **Global admin** (global `admin`) | Everything in every weblog; users; site settings; Planet feeds; ping targets | Get shell or file access on the host | Yes |
+| **Network attacker** | Read or change traffic when the site runs over plain HTTP | Anything, once the site runs over HTTPS | No |
+| **Operator** | Owns the container, JVM, database, configuration files and logs | — | Yes, out of scope |
+
+### Why the weblog Admin is untrusted
+
+This is the most important line in the model. On a multi-user site, anyone who
+can register can create a weblog and become its Admin. If weblog Admins were
+trusted, every registered user would be trusted.
+
+Roller's defaults reflect this. `weblogAdminsUntrusted=true` turns on HTML
+sanitizing, and templates run in a sandbox built for untrusted authors.
+
+A request forged cross-site that makes a logged-in user's browser act is an
+attack by an anonymous visitor, not by that user.
+
+## Where untrusted data enters
+
+| Input | Who controls it |
+| --- | --- |
+| Any request to public pages, feeds and search | Anyone |
+| Comments: name, email, URL, text | Anyone |
+| Entry text, titles and summaries | Weblog members |
+| Categories, bookmarks, profiles, weblog settings | Weblog members and Admins |
+| Templates and stylesheets | Weblog Admins, when custom themes are on |
+| Uploaded files: content, name, declared type | Weblog members, when uploads are on |
+| OPML bookmark import | Weblog Admins |
+| Remote API requests (XML-RPC, AtomPub, OAuth) | Anyone, before authentication |
+| Feeds fetched by Planet | Whoever runs the feed's host |
+| Responses from ping targets | Whoever runs the ping target |
+| Installer forms | Anyone, until setup is complete |
+
+Only the operator controls `roller-custom.properties`, JNDI resources and the
+database.
+
+## The four boundaries
+
+```mermaid
+flowchart LR
+  V["Visitor / commenter"] -->|"① request data"| R["Public pages,<br/>comments, search"]
+  M["Member of weblog A"] -->|"② allowed"| A[("Weblog A")]
+  M -. "② blocked" .-> B[("Weblog B")]
+  WA["Weblog Admin"] -->|"③ HTML, templates, uploads"| S["Sanitizer,<br/>sandbox,<br/>media policy"]
+  S --> P["Pages that every<br/>visitor's browser runs"]
+  U["Any user below<br/>global admin"] -. "④ blocked" .-> N(("Network"))
+  GA["Global admin"] -->|"④ allowed"| N
+```
+
+1. **Visitor → server.** All request data is untrusted. Roller escapes what it
+   writes back into pages, never runs request data as code, and reads XML as
+   data only. One request that causes runaway CPU or memory use (for example
+   catastrophic regex backtracking) is a vulnerability. A slow response is not.
+2. **Weblog → weblog.** A role in weblog A gives nothing in weblog B. This
+   includes every object named by id in a request: entries, comments, media,
+   templates, categories, bookmarks and members. A global admin holds every role
+   in every weblog.
+3. **Weblog author → everyone else.** Content that members and Admins write
+   runs in other people's browsers. Roller keeps it from attacking visitors,
+   other users or the server:
+   - HTML in entries, profiles and settings is sanitized when
+     `weblogAdminsUntrusted=true`.
+   - Comments are always escaped, or limited to a small HTML subset. Comment
+     links and author URLs must be `http` or `https`.
+   - Templates run in the sandbox.
+   - Uploaded files are never served as active content (see below).
+4. **Server → network.** Users below global admin cannot make the server fetch
+   a URL of their choice. Entry enclosure URLs are stored, not fetched. Global
+   admins can point the server at any URL through Planet feeds and ping targets.
+   That is by design.
+
+Two more rules protect the whole site:
+
+- **Accounts.** Passwords are stored as bcrypt hashes. Roller takes the user's
+  identity from the login, never from request data. A password change ends the
+  user's other sessions.
+- **Setup.** Before setup, the installer needs a one-time token that Roller
+  writes to the server log. Once setup is complete, the installer is closed for
+  good.
+
+## How a request is checked
+
+Every request to the authoring UI passes these checks in order. A missing check
+creates a predictable class of bug.
+
+```mermaid
+flowchart TD
+  Q["Request to /roller-ui/authoring/..."] --> G1{"Logged in with a global role?"}
+  G1 -- no --> X1["Redirect to login"]
+  G1 -- yes --> G2{"Holds the required role<br/>in THIS weblog?"}
+  G2 -- no --> X2["Denied<br/>(missing: privilege escalation)"]
+  G2 -- yes --> G3{"Does every object named<br/>in the request belong<br/>to this weblog?"}
+  G3 -- no --> X3["Denied<br/>(missing: cross-weblog access)"]
+  G3 -- yes --> G4{"Valid one-time salt<br/>on a state change?"}
+  G4 -- no --> X4["Rejected<br/>(missing: CSRF)"]
+  G4 -- yes --> OK["Action runs"]
+```
+
+The remote APIs (XML-RPC, AtomPub) authenticate with a username and password,
+or OAuth, instead of a session and salt. They apply the same role and object
+checks.
+
+## How content is kept safe
+
+```mermaid
+flowchart LR
+  subgraph In["Written by untrusted users"]
+    E["Entry HTML,<br/>profiles, settings"]
+    C["Comments"]
+    T["Templates"]
+    U["Uploads"]
+  end
+  E --> SAN["HTML sanitizer<br/>(weblogAdminsUntrusted=true)"]
+  C --> ESC["Escaped, or HTML subset;<br/>links http/https only"]
+  T --> SB["Velocity sandbox:<br/>no arbitrary Java calls,<br/>no reads outside the theme"]
+  U --> MT["Type taken from file name;<br/>active types never inline;<br/>nosniff"]
+  SAN --> OUT["Page sent to visitors"]
+  ESC --> OUT
+  SB --> OUT
+  MT --> OUT
+```
+
+Uploaded files get their type from the file name, not from what the browser
+claims. Only passive types such as images, audio, video, PDF and plain text are
+shown inline. Everything else, including HTML and SVG, is sent as a download.
+
+## Settings
+
+```mermaid
+flowchart LR
+  subgraph Supported["Optional features: supported when enabled"]
+    S1["XML-RPC"]
+    S2["AtomPub"]
+    S3["Uploads"]
+    S4["Custom themes"]
+    S5["Self-registration"]
+    S6["HTML comments"]
+    S7["LDAP / OpenID login"]
+    S8["Remember-me"]
+    S9["Planet aggregator"]
+  end
+  subgraph Unsupported["Unsupported on a multi-user site"]
+    U1["weblogAdminsUntrusted=false"]
+    U2["authentication.method=cma"]
+    U3["passwds.encryption.enabled=false"]
+  end
+```
+
+A bug in an optional feature is a vulnerability when the feature is enabled.
+
+| Setting | What it changes |
+| --- | --- |
+| `weblogAdminsUntrusted` | `true` (default) sanitizes HTML from weblog members. Set `false` only when every weblog admin is trusted, such as on a solo blog. |
+| `themes.customtheme.allowed` | Lets weblog Admins edit templates. Templates stay in the sandbox. |
+| `uploads.enabled` | Lets members upload media files. |
+| `webservices.enableXmlRpc`, `webservices.enableAtomPub` | Turn on the remote publishing APIs. They use the same weblog roles as the UI. |
+| `users.registration.enabled` | Lets anyone create an account. |
+| `users.comments.htmlenabled` | Allows a small HTML subset in comments. |
+| `authentication.method` | `db`, `ldap`, `openid` or `db-openid`. `cma` is not usable. |
+| `rememberme.enabled` | Keeps users logged in with a cookie. Use only over HTTPS. |
+| `passwds.encryption.enabled` | `false` stores plaintext passwords. Not supported. |
+
+## Libraries
+
+Roller decides how it configures and calls each library. A library bug hit
+while Roller uses the library correctly belongs to that library's project. Roller
+passing untrusted input to a library in an unsafe way is a Roller bug.
+
+## Not vulnerabilities
+
+Roller does not protect against:
+
+- **The operator or host**, or anyone who can edit the configuration or the
+  database.
+- **A global admin misusing the admin UI.** A request forged against a logged-in
+  global admin is still a vulnerability.
+- **Plain HTTP.** TLS is the operator's job.
+- **Floods, brute-force login attempts and spam.** Roller has no rate limit or
+  login lockout. The comment math question is an anti-spam measure, not
+  authentication.
+- **Browsers out of vendor support.** Script that runs only in such browsers
+  (for example old Internet Explorer modes) is not a vulnerability. A payload
+  that also runs in a current browser is.
+- **Forged anonymous comments.** A comment forged cross-site carries no
+  identity, so it gains the attacker nothing.
+- **Unsupported settings**, listed under Settings above.
+- **Partial writes** when a multi-step operation fails. That is a bug, not a
+  vulnerability.
+
+## Operator checklist
+
+- Serve Roller only over HTTPS, and set the session cookie `Secure` flag in the
+  servlet container. Roller sets `HttpOnly` but not `Secure`.
+- Keep `weblogAdminsUntrusted=true` unless every weblog admin is trusted.
+- Enable custom themes, uploads, XML-RPC and AtomPub only when you need them.
+- Restrict access to the server log. It holds the setup token during
+  installation and upgrades.
+- Keep the database, data directories and `roller-custom.properties` private.
+- If the server can reach internal services, put egress rules on Roller's
+  outbound traffic (Planet, pings).
+- Run the current release. Older releases are not patched.
+
+## Common mistakes
+
+- Setting `weblogAdminsUntrusted=false` on a multi-user site to allow rich HTML.
+- Enabling custom themes and assuming templates are harmless. They are
+  sandboxed, but they run on every page view.
+- Running over plain HTTP with remember-me enabled.
+- Running setup on a host where other people can read the log.
+
+## Judging a report
+
+```mermaid
+flowchart TD
+  A["Report"] --> B{"Who is the attacker?<br/>(see The users)"}
+  B -- "Operator, or a global admin<br/>acting directly" --> OUT1["Not a vulnerability"]
+  B -- "Anyone else, or a request<br/>forged against an admin" --> C{"Does it need an<br/>unsupported setting?"}
+  C -- yes --> OUT2["Not a vulnerability"]
+  C -- no --> D{"Does it cross one of<br/>the four boundaries?"}
+  D -- yes --> VALID["Vulnerability"]
+  D -- no --> E{"Listed under<br/>Not vulnerabilities?"}
+  E -- yes --> BD["Not a vulnerability"]
+  E -- no --> GAP["Unclear: the PMC decides<br/>and updates this model"]
+```