Tiny RSS

tt-rss.org
Tiny RSS

A web-based news feed reader and aggregator, supporting RSS/Atom feeds. It's free, open source, and offers a customizable and self-hostable platform for managing your news feeds.

Open Source

Tiny RSS Source Code

Author

tt-rss

Description

A free, flexible, open-source, web-based news feed (RSS/Atom/other) reader and aggregator.

#rss#rss-aggregator#rss-generator#tt-rss#ttrss

Homepage

Repository

  • LicenseGPL-3.0
  • Created03 Oct 25
  • Primary languagePHP
  • Size137,659 KB
  • Stars814
  • Forks84
  • Watchers814

Language Usage

Language Usage

Project Health

  • Last commit3 days ago
  • Open issues25

Recent Commits

  • Greg(13 Aug 26)

    Add a basic check for curl before setting related props. (#427)

  • Greg(12 Aug 26)

    Restore support for compressed responses in 'UrlHelper::fetch()'. (#425)

  • supahgreg(12 Aug 26)

    Add some tests for #424 (allowing standard ports to IPv6 ULA).

  • Thomas Wucher(12 Aug 26)

    Make handling of IPv6 addresses symmetric to IPv4 (#424) Local IPv4 addresses are already accepted when a standard port is used. Do the same for local IPv6 addresses.

  • Andrew Gaul(11 Aug 26)

    Compress and cache lib/flat-ttrss's icon font (#423) The theme draws dijit's tab, arrow, checkbox and validation glyphs from lib/flat-ttrss/fonts/flat-icon, so every page load fetches it -- and it arrived uncompressed and was revalidated each time. nginx's mime.types has no entry for .ttf, so the font fell back to default_type and went out as application/octet-stream. Since gzip_types can only match a type the response actually carries, it takes the types entry as well before it has any effect: 13436 -> 4689 bytes. woff and woff2 stay out of gzip_types, already being compressed internally. The font is versioned too, as flat-icon.ttf?90nq1s, but the immutable match accepted only digits and so left it revalidating. Widening that to bare alphanumerics still cannot match a key=value pair, for want of an '='; across an access log of 199 distinct static query strings it promotes this font and nothing else. Unlike ?filemtime(), a baked build token is not derived from the file it versions, so a vendored asset replaced without regenerating the stylesheet that names it will now stay cached for a year. Co-authored-by: Claude Opus 5 (1M context) <[email protected]>

  • Andrew Gaul(10 Aug 26)

    Send Cache-Control for static assets (#419) Static assets went out with only Last-Modified and ETag, no Cache-Control at all, which leaves each browser to invent a freshness lifetime and revalidate once its guess runs out. A warm repeat page load spent 8 conditional requests confirming nothing had changed -- all latency, no payload, and on a phone that is the expensive kind of request. Most asset URLs are already content-versioned and need none of it: javascript_tag() and stylesheet_tag() append ?filemtime(), get_theme_path() does the same, and dojo's loader appends dojoConfig.cacheBust to the modules it fetches lazily. Those get a year and immutable, which takes the same repeat load down to 2 requests, both for assets that carry no version (the favicons and the web manifest). The rule keys on the query string rather than the path, which matters: about 6% of the dojo module fetches in an access log arrive with no version at all, so a rule like "location ~ \.js$ { expires 1y; }" would serve stale JavaScript for a year after an upgrade. Keyed this way, an unversioned URL falls through to no-cache and keeps revalidating, which is what it needs to do. Deliberately without add_header's "always" flag. With it the header also lands on error responses, so a 404 for a versioned URL -- an asset briefly missing mid-deploy -- would be cached as immutable for a year, leaving a broken app that no reload can fix. nginx's default status list still includes 304, which is required for browsers to refresh the freshness lifetime. One precondition worth knowing: get_scripts_timestamp(), which feeds dojoConfig.cacheBust, globs only js/*.js. Updating a vendored file under lib/ without touching anything in js/ leaves the cacheBust value unchanged, so lazily-loaded lib modules would keep serving from cache. Widening that glob would remove the caveat. It does not affect lib/dojo/dojo.js or lib/dojo/tt-rss-layer.js, which are versioned per-file by javascript_tag(). Co-authored-by: Claude Opus 5 (1M context) <[email protected]>

  • Andrew Gaul(10 Aug 26)

    Stop sending no-store on the HTML shells (#421) index.php and prefs.php were sent with Expires: Thu, 19 Nov 1981 08:52:00 GMT Cache-Control: no-store, no-cache, must-revalidate Pragma: no-cache Nothing here asked for that: it is PHP's default session.cache_limiter of 'nocache', emitted by the session_start() in include/sessions.php. The 1981 date is a constant in PHP's session extension. There is no session_cache_limiter() call anywhere in the tree, and DiskCache.php:337 already works around these headers locally rather than changing the default. What the shells require is that no shared cache store them -- they embed the session's CSRF token, the user's theme and stylesheet and per-plugin CSS -- and that they be revalidated before reuse. That is 'private, no-cache'. no-store adds only "never write it to disk", which guards the CSRF token while leaving the session cookie that token is bound to in the cookie store regardless, so it buys nothing here. It does cost something. no-store disqualifies a document from the back/forward cache (Firefox excludes it outright over HTTPS; Chrome has historically done the same), so leaving the app and returning re-boots the whole client: fresh HTML, dojo re-require of every module, module registry rebuilt. On a phone that is an ordinary back gesture. Since the shells are byte-stable for the life of a session, they can also carry a validator, so a reload becomes an empty 304 instead of a retransmission. send_conditional_html() hashes the buffered document; hashing the whole body is what makes this safe, because every session-specific value in the page feeds the hash and a 304 can therefore only reach a client that already holds this exact session's copy. Verified: two concurrent sessions get different tags, and one session's tag against the other returns 200. The limiter opt-out is per entry point, not an ini change -- api/index.php and the public handlers share the session machinery but not these requirements, and they keep PHP's default. Handler_Public::_render_login_form() had the opposite bug: 'Cache-Control: public' on a form that renders $_SESSION['login_error_msg'] (then clears it) and auth_remote's prefilled $_SESSION['fake_login']. A shared cache was permitted to store one visitor's copy and replay it, disclosing that login name and why their session failed, and re-showing an error the server had already cleared. No CSRF exposure -- the login form carries no token and login() does not validate one. Measured against the running stack, authenticated: before 200, no-store, no validator, 4212 B gzipped every load after 200, private/no-cache + ETag, then 304 with 0 B on reload Dropping the limiter also drops its HTTP/1.0 belt-and-braces (the past Expires and response Pragma). 'private' already bars shared caches under HTTP/1.1, and RFC 9111 deprecates Pragma as a response header. Co-authored-by: Claude Opus 5 (1M context) <[email protected]>

  • Derek Schrock(10 Aug 26)

    Test for the UTF-8 text HTML entity depending libxml version (#415)

  • Andrew Gaul(10 Aug 26)

    Tune the existing gzip configuration (#417) nginx defaults gzip_comp_level to 1, and the config never overrode it, so tt-rss has been shipping barely-compressed responses. Level 6 is the knee of the curve: on the headlines JSON it captures 15.5% of the 16.0% available from level 9. Levels 7-9 are only worth paying for offline, so gzip_static is enabled alongside utils/precompress-assets.sh, which writes level 9 siblings at build time. image/svg+xml was missing from gzip_types. images/three-dots.svg is on the critical path for every page load and was being sent uncompressed at 1513 bytes; it is 380 on the wire now. gzip_min_length drops from 1024 to 256 so the smaller SVGs are covered too, and .webmanifest gains a mime type rather than being served as application/octet-stream. Measured against the previous configuration, over real responses: cold load (718 KB of JS/CSS/SVG/HTML) 244,672 -> 201,702 -17.6% headlines JSON, per feed view 20,265 -> 17,124 -15.5% This costs less CPU rather than more, because gzip_static stops nginx recompressing the same static assets on every request, and that dominates: 486 KB of JavaScript 3.40 -> 0.08 ms CPU/request headlines JSON 0.85 -> 1.51 ms CPU/request full page load 6.71 -> 2.62 ms CPU/page The JSON is the one case that gets more expensive, since PHP output cannot be precompressed. For scale, generating that same response costs PHP-FPM 12.8ms, so the extra 0.66ms is about 5% of the request's total server cost. Worker memory is unchanged (measured on fresh containers under identical load). Two things that came out of measuring rather than assuming: gzip_min_length only ever applied to static files. PHP responses come back over FastCGI with Transfer-Encoding: chunked and no Content-Length, so nginx cannot know their size and compresses them whatever the setting says. A side effect is that the 60-second counter poll, 76 bytes of JSON, goes out as 91 bytes: below roughly 200 bytes the gzip header costs more than it saves. Fixing that would mean emitting Content-Length from PHP, which is out of scope here. fastcgi_buffers deliberately left alone. The chunked delivery does cost ratio in principle, but for gzip the effect is negligible: raising the buffers from 32k to 2M per request moved the same response only from 17,150 to 17,124 bytes, against 17,112 compressing the whole body offline. Co-authored-by: Claude Opus 5 (1M context) <[email protected]>

  • Andrew Gaul(10 Aug 26)

    Fix phone-layout toolbar dropdowns dismissing themselves when tapped. (#412) dijit Selects (and every other _HasDropDown opener) open their popup on pointerdown, which parks the modal popup backdrop under the still-down finger; when the finger lifts, the browser hit-tests the tap's synthesised compatibility click against the current DOM, so it landed on the backdrop and instantly closed the menu it had just opened. dijit Buttons were immune -- a11yclick sets dojoClick on them, making dojo/touch swallow the tap's compatibility events -- which is why the Actions menu worked while the view mode / sort order dropdowns did not. Long-presses produce no compatibility click, which is why tap-and-hold appeared to work. Only honour backdrop clicks preceded by a pointerdown that actually landed on the backdrop: a ghost click can never qualify (its press hit the opener before the backdrop existed), while real outside taps and mouse clicks still dismiss the menu as before. Co-authored-by: Claude Fable 5 <[email protected]>

  • Greg(06 Aug 26)

    Improve lookups in 'UrlHelper::has_disallowed_ip()', handle some more issues. (#414)

  • Derek Schrock(04 Aug 26)

    Fix deprecated warning with parse_url within get_self_url (#411)

  • supahgreg(03 Aug 26)

    Avoid excessive caching with the app image build.

  • Greg(03 Aug 26)

    Merge pull request #408 from tt-rss/guzzle-8 Bump 'guzzlehttp/guzzle' to 8.0.1.

  • Greg(03 Aug 26)

    Address IPv4/host canonicalization bypasses in 'UrlHelper::has_disallowed_ip()'. GHSA-pfj9-qgh4-9wm (#409) * Address IPv4/host canonicalization bypasses in 'UrlHelper::has_disallowed_ip()'. GHSA-pfj9-qgh4-9wm * Temporarily disable Rector rule 'ExplicitPublicClassMethodRector'.

  • supahgreg(02 Aug 26)

    Use Guzzle 8 exception classes, drop usage of removed method. 'RequestException::getHandlerContext()' was removed with Guzzle 8.

  • supahgreg(02 Aug 26)

    Drop NTLM as an auth type. NTLM support has already been dropped from Alpine's libcurl build since April 2026, and curl plans to fully drop support in September 2026. https://github.com/guzzle/guzzle/blob/8.0/UPGRADING.md#auth-request-option-changes

  • supahgreg(02 Aug 26)

    Bump 'guzzlehttp/guzzle' to 8.0.1.

  • dependabot[bot](02 Aug 26)

    GitHub Actions: Bump docker/login-action from 4.4.0 to 4.6.0 (#407) Bumps [docker/login-action](https://github.com/docker/login-action) from 4.4.0 to 4.6.0. - [Release notes](https://github.com/docker/login-action/releases) - [Commits](https://github.com/docker/login-action/compare/af1e73f918a031802d376d3c8bbc3fe56130a9b0...dbcb813823bdd20940b903addbd779551569679f) --- updated-dependencies: - dependency-name: docker/login-action dependency-version: 4.6.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] <[email protected]> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

  • dependabot[bot](02 Aug 26)

    npm: bump the development-dependencies group with 4 updates (#404) Bumps the development-dependencies group with 4 updates: [eslint](https://github.com/eslint/eslint), [globals](https://github.com/sindresorhus/globals), [less](https://github.com/less/less.js) and [stylelint](https://github.com/stylelint/stylelint). Updates `eslint` from 10.6.0 to 10.8.0 - [Release notes](https://github.com/eslint/eslint/releases) - [Commits](https://github.com/eslint/eslint/compare/v10.6.0...v10.8.0) Updates `globals` from 17.7.0 to 17.8.0 - [Release notes](https://github.com/sindresorhus/globals/releases) - [Commits](https://github.com/sindresorhus/globals/compare/v17.7.0...v17.8.0) Updates `less` from 4.6.7 to 4.8.1 - [Release notes](https://github.com/less/less.js/releases) - [Changelog](https://github.com/less/less.js/blob/master/CHANGELOG.md) - [Commits](https://github.com/less/less.js/compare/v4.6.7...v4.8.1) Updates `stylelint` from 17.14.0 to 17.14.1 - [Release notes](https://github.com/stylelint/stylelint/releases) - [Changelog](https://github.com/stylelint/stylelint/blob/main/CHANGELOG.md) - [Commits](https://github.com/stylelint/stylelint/compare/17.14.0...17.14.1) --- updated-dependencies: - dependency-name: eslint dependency-version: 10.8.0 dependency-type: direct:development update-type: version-update:semver-minor dependency-group: development-dependencies - dependency-name: globals dependency-version: 17.8.0 dependency-type: direct:development update-type: version-update:semver-minor dependency-group: development-dependencies - dependency-name: less dependency-version: 4.8.1 dependency-type: direct:development update-type: version-update:semver-minor dependency-group: development-dependencies - dependency-name: stylelint dependency-version: 17.14.1 dependency-type: direct:development update-type: version-update:semver-patch dependency-group: development-dependencies ... Signed-off-by: dependabot[bot] <[email protected]> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

  • dependabot[bot](02 Aug 26)

    GitHub Actions: Bump actions/checkout from 7.0.0 to 7.0.1 (#401) Bumps [actions/checkout](https://github.com/actions/checkout) from 7.0.0 to 7.0.1. - [Release notes](https://github.com/actions/checkout/releases) - [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md) - [Commits](https://github.com/actions/checkout/compare/9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0...3d3c42e5aac5ba805825da76410c181273ba90b1) --- updated-dependencies: - dependency-name: actions/checkout dependency-version: 7.0.1 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] <[email protected]> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

  • dependabot[bot](02 Aug 26)

    GitHub Actions: Bump actions/setup-node from 6.4.0 to 7.0.0 (#396) Bumps [actions/setup-node](https://github.com/actions/setup-node) from 6.4.0 to 7.0.0. - [Release notes](https://github.com/actions/setup-node/releases) - [Commits](https://github.com/actions/setup-node/compare/48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e...820762786026740c76f36085b0efc47a31fe5020) --- updated-dependencies: - dependency-name: actions/setup-node dependency-version: 7.0.0 dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] <[email protected]> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

  • Andrew Gaul(02 Aug 26)

    Render CDM article content from the Headlines cache, not row attributes (#393) Combined-mode rows carried every article's HTML in up to three places: the raw string in the Headlines.headlines cache, an escaped copy in the row's data-content attribute (plus data-rendered-enclosures), and -- in expandable mode -- another write-only attribute copy saved to data-content-original on every unpacked row. Article.unpack() now renders from the cache, making it the single retained copy, and the dead data-content-original write is removed. Besides the redundant copies this drops the App.escapeHtml() call over full article bodies (~36% size inflation) from the row-render hot path and roughly halves the innerHTML parsed per row (avg 9.1KB -> 4.1KB on the dev feeds). At a 210-headline session that is about 1MB of renderer-side attribute strings. Co-authored-by: Claude Fable 5 <[email protected]>

  • Greg(19 Jul 26)

    Exclude newer test dirs in the Publish workflow

  • Greg(18 Jul 26)

    minor: fix a test copy+paste mistake

  • supahgreg(16 Jul 26)

    Bump the 'web-nginx' base image to nginx 1.31.3. https://nginx.org/en/CHANGES

  • supahgreg(15 Jul 26)

    Bump 'guzzlehttp/guzzle' to 7.14.2.

  • Greg(15 Jul 26)

    Disallow the 'srcdoc' attribute. GHSA-mr5j-8f39-q8hh (#394)

  • dependabot[bot](05 Jul 26)

    GitHub Actions: Bump docker/build-push-action from 7.2.0 to 7.3.0 (#389) Bumps [docker/build-push-action](https://github.com/docker/build-push-action) from 7.2.0 to 7.3.0. - [Release notes](https://github.com/docker/build-push-action/releases) - [Commits](https://github.com/docker/build-push-action/compare/f9f3042f7e2789586610d6e8b85c8f03e5195baf...53b7df96c91f9c12dcc8a07bcb9ccacbed38856a) --- updated-dependencies: - dependency-name: docker/build-push-action dependency-version: 7.3.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] <[email protected]> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

  • dependabot[bot](05 Jul 26)

    GitHub Actions: Bump docker/setup-qemu-action from 4.1.0 to 4.2.0 (#388) Bumps [docker/setup-qemu-action](https://github.com/docker/setup-qemu-action) from 4.1.0 to 4.2.0. - [Release notes](https://github.com/docker/setup-qemu-action/releases) - [Commits](https://github.com/docker/setup-qemu-action/compare/06116385d9baf250c9f4dcb4858b16962ea869c3...96fe6ef7f33517b61c61be40b68a1882f3264fb8) --- updated-dependencies: - dependency-name: docker/setup-qemu-action dependency-version: 4.2.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] <[email protected]> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

Tiny RSS Security

Security Advisories (2)

  • mediumPatchedCVSS 4.3

    GHSA-pfj9-qgh4-9wmqTiny Tiny RSS URL validation permits internal-address SSRF bypasses

  • highPatchedCVSS 8

    GHSA-mr5j-8f39-q8hhStored cross-site scripting via iframe srcdoc in YouTube-whitelisted feed content

Tiny RSS Website

Website

Home | Tiny Tiny RSS

Tiny Tiny RSS (tt-rss) is a free, flexible, open-source, web-based news feed (RSS/Atom/other) reader and aggregator.

Redirects

Does not redirect

Security Checks

All 65 security checks passed

Server Details

  • IP Address185.199.110.153
  • Hostnamecdn-185-199-110-153.github.com
  • LocationFrancisco,Indiana,United States of America,NA
  • ISPGitHub Inc.
  • ASNAS54113

Associated Countries

  • USUS

Safety Score

Website marked as safe

100%

Blacklist Check

tt-rss.org was found on 0 blacklists

  • AntiSocial Blacklist
  • Artists Against 419
  • Badbitcoin
  • Bambenek Consulting
  • CERT Polska
  • CoinBlockerLists
  • CRDF
  • CryptoScamDB
  • EtherAddressLookup
  • EtherScamDB
  • Fake Website Buster
  • MetaMask EthPhishing
  • NABP Not Recommended Sites
  • OpenPhish
  • PetScams
  • PhishFeed
  • PhishFort
  • Phishing.Database
  • PhishStats
  • PhishTank
  • Phishunt
  • RPiList Not Serious
  • Scam.Directory
  • SecureReload Phishing List
  • Spam404
  • StopGunScams
  • Suspicious Hosting IP
  • ThreatFox
  • ThreatLog
  • TweetFeed
  • URLhaus
  • ViriBack C2 Tracker

Website Preview

Website preview

Tiny RSS Docker

Container Info

tt-rss

Tiny Tiny RSS is an open source web-based news feed (RSS/Atom) reader and aggregator, designed to allow you to read news from any location, while feeling as close to a real desktop application as possible.

#Other

View on DockerHub

lunik1/tt-rss:latest

Run Command

docker run -d \
  -p 80/tcp \
  -e PUID=${PUID} \
  -e PGID=${PGID} \
  -v /portainer/Files/AppData/Config/tt-rss:/config \
  --restart=unless-stopped \
  lunik1/tt-rss:latest

Compose File

version: 3.8
services:
  tiny-tiny-rss:
    image: "lunik1/tt-rss:latest"
    ports:
      - 80/tcp
    environment:
      PUID: 1000
      PGID: 100
    volumes:
      - "/portainer/Files/AppData/Config/tt-rss:/config"
    restart: unless-stopped

Environment Variables

  • Var NameDefault
  • PUID1000
  • PGID100

Port List

  • 80/tcp

Volume Mounting

  • Container PathHost Bind
  • /config/portainer/Files/AppData/Config/tt-rss

Tiny RSS Reviews

More News Readers

⚠️ This section is still a work in progress ⚠️
Check back soon, or help us complete it by submiting a pull request on GitHub.
Or submit an entry here

About the Data: Tiny RSS

Change History

Edit Tiny RSS Data

You can edit Tiny RSS's entry in this section of awesome-privacy.yml by submitting a PR to our GitHub repo.
Note that some of the information shown above has been aggregated from external sources, a list of these can be found data documentation.

Origin Data

Modify Data

API

You can access Tiny RSS's data programmatically via our API. Simply make a GET request to:

https://api.awesome-privacy.xyz/v1/services/tiny-rss

The REST API is free, no-auth and CORS-enabled. To learn more, view the API Docs or read the API Usage Guide.

Share Tiny RSS

Help your friends compare News Readers, and pick privacy-respecting software and services.
Share Tiny RSS and Awesome Privacy with your network!