Skip to content

v2.12.0

Latest

Choose a tag to compare

@sean-e-dietrich sean-e-dietrich released this 31 Jul 16:10
cfcc821

Configuration can now read a file from a path you write directly, without routing it through an environment variable.

challenge:
  secret: '%file(/etc/firewall/hmac.key)%'

Nothing else changes. No new dependencies, no behaviour changes, and the existing %env(...)% syntax is untouched.

PHP 8.1–8.5 supported.

Added

  • %file(/path)% token (#118, fixes #116). Reads a file into a config value from a literal path.

    Every previous route ran through an environment variable, because the variable name is always the last colon-separated segment of an %env(...)% token. Someone holding a path — a mounted secret, a key file placed by configuration management — had one option, and it was a workaround:

    secret: '%env(file:default:/etc/firewall/hmac.key:UNUSED)%'

    That still works and you will meet it in existing configs, but it carries two hazards the new token does not:

    • It depends on a variable never existing. UNUSED has to stay undefined forever. If anything later defines it — a platform injecting variables, a .env file, a colleague reusing the name — the path silently becomes that value, and nothing in the config file signals the dependency.
    • A colon truncates the path. Tokens are split on :, so /tmp/sec:rets/key.txt resolves as /tmp/sec. This is the same limitation raw_key: exists to work around for keys; there was no equivalent escape for paths.

    With %file(...)% the token content is the whole path, so colons are ordinary characters and there is no variable to keep undefined.

    It interpolates inside larger strings and resolves inside nested arrays, exactly as %env(...)% does, and returns contents verbatim — newline included — matching the file: processor. A key file usually ends with \n, and an HMAC secret carrying a stray newline fails in a way that is annoying to diagnose; read it through a configuration override if you need it trimmed.

    The security posture is unchanged. It is held to the same opt-in and base-directory allowlist as the existing file: processor:

    TokenSubstitute::enableUnsafeProcessors(['file'], ['/etc/firewall']);

    There is a reasonable argument for exempting it — a literal path in a config file is only as trustworthy as that file, and an attacker who can edit your firewall config has better options than a file read. It is held to the same controls anyway, so there is one mental model rather than two, and so the allowlist still limits the blast radius if a config file is ever compromised.

    No %require(...)% counterpart. Executing a literal path is still executing a path, and the guidance to prefer file: over require: stands.

Documentation

  • Reading a file from a path is documented at all (#117, fixes #114). The building blocks were each described separately, but nothing connected them, so anyone holding a path rather than a variable had no documented route. The Filesystem Processors section now covers it directly.

  • The opt-in rationale was overstated. It read "Because their path comes from an environment variable…", which is true of the common usage but reads as a requirement. It is not one — and the literal form is the safer of the two, since an attacker who controls the environment cannot redirect a path written in the config file. Reworded, with the literal form pointed to as the lower-risk option.

  • The colon limitation is now written down, beside the existing raw_key: note that covers the same hazard for keys.

  • overrides.md shows reading a value from a file. Its examples only ever read $_ENV, so the simplest route for a secret from a known path — file_get_contents() in an override, needing no opt-in, no allowlist and no token parsing at all — was invisible. The trim() in that example is explained rather than merely present.

Upgrade notes

  • composer update kanopi/firewall, and nothing else. No configuration changes, no new dependencies, and no existing token or rule changes meaning.

  • %file(...)% needs the same opt-in as file:. If you have not called TokenSubstitute::enableUnsafeProcessors(['file'], [...]) during bootstrap, the token raises ConfigurationException rather than reading anything. Pass real base directories — an empty allowlist disables the prefix check entirely.

  • Existing %env(file:default:…)% config keeps working. Nothing is deprecated. Moving to %file(...)% is worthwhile if your path contains a colon, or if you would rather not depend on a variable staying undefined — and if you are on the old form, make sure your allowlist is not empty, because it is what stops a hijacked variable from redirecting the read.

Internal

  • 14 tests for the new token. Beyond the obvious cases they pin the two hazards it removes: one asserts a colon-containing path works, and another asserts the old form still fails on that same path, so the first is measuring a real difference rather than a coincidence. A third sets the workaround's variable to /etc/passwd and confirms the new token is unaffected.

  • Refusal without the opt-in, paths outside the allowlist, and traversal out of the allowlist are all covered, as is coexistence — both token forms resolving in the same string.

  • Suite: 1,029 unit tests and 49 integration tests pass. PHPCS, PHPStan at level max and Rector are clean, and bin/firewall-check is clean at level max despite sitting outside the src/-scoped gates. The 2 reported deprecations are a pre-existing ReflectionProperty::setValue() call in LoggingFactoryTest, unrelated to this release.